@anweat/dsh-browser 0.1.7 → 0.1.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 (53) hide show
  1. package/README.md +306 -245
  2. package/cordis.patch.yml +26 -16
  3. package/lib/approval-policy.js +15 -0
  4. package/lib/approval-policy.js.map +1 -1
  5. package/lib/browser-service.d.ts +68 -25
  6. package/lib/browser-service.js +222 -62
  7. package/lib/browser-service.js.map +1 -1
  8. package/lib/client/SettingsCard.d.ts +2 -0
  9. package/lib/client/SettingsCard.js +21 -0
  10. package/lib/client/SettingsCard.js.map +1 -0
  11. package/lib/client/context-types.d.ts +8 -0
  12. package/lib/client/context-types.js +2 -0
  13. package/lib/client/context-types.js.map +1 -0
  14. package/lib/client/form.d.ts +62 -0
  15. package/lib/client/form.js +198 -0
  16. package/lib/client/form.js.map +1 -0
  17. package/lib/client/index.d.ts +16 -0
  18. package/lib/client/index.js +18 -0
  19. package/lib/client/index.js.map +1 -0
  20. package/lib/client/locales.d.ts +58 -0
  21. package/lib/client/locales.js +55 -0
  22. package/lib/client/locales.js.map +1 -0
  23. package/lib/client/settings-namespace.d.ts +2 -0
  24. package/lib/client/settings-namespace.js +3 -0
  25. package/lib/client/settings-namespace.js.map +1 -0
  26. package/lib/client/styles.d.ts +37 -0
  27. package/lib/client/styles.js +25 -0
  28. package/lib/client/styles.js.map +1 -0
  29. package/lib/client.js +753 -0
  30. package/lib/client.js.map +1 -0
  31. package/lib/config.d.ts +10 -0
  32. package/lib/config.js +22 -0
  33. package/lib/config.js.map +1 -1
  34. package/lib/deps.d.ts +7 -1
  35. package/lib/deps.js +22 -6
  36. package/lib/deps.js.map +1 -1
  37. package/lib/freedom.d.ts +1 -1
  38. package/lib/freedom.js +4 -0
  39. package/lib/freedom.js.map +1 -1
  40. package/lib/index.js +13 -1
  41. package/lib/index.js.map +1 -1
  42. package/lib/opencli-catalog.d.ts +21 -0
  43. package/lib/opencli-catalog.js +49 -0
  44. package/lib/opencli-catalog.js.map +1 -0
  45. package/lib/scripts.js +27 -27
  46. package/lib/tools.js +75 -1
  47. package/lib/tools.js.map +1 -1
  48. package/lib/usage-policy.d.ts +49 -0
  49. package/lib/usage-policy.js +171 -0
  50. package/lib/usage-policy.js.map +1 -0
  51. package/package.json +133 -78
  52. package/scripts/check-client-bundle.mjs +15 -0
  53. package/scripts/clean-lib.mjs +3 -0
@@ -3,6 +3,7 @@ import { recipeNeedsApproval } from "./automation.js";
3
3
  import { validateUserscript } from "./scripts.js";
4
4
  import { isBrowserToolExposed } from "./freedom.js";
5
5
  const DIRECT_INTERACTIONS = new Set(['browser_click', 'browser_type', 'browser_scroll']);
6
+ const WEB_LOCAL_MUTATIONS = new Set(['web_cache_clear']);
6
7
  export function browserPolicyDecision(name, args, mode = 'standard') {
7
8
  if (!isBrowserToolExposed(name, mode) && name.startsWith('browser_')) {
8
9
  return { kind: 'deny', reason: `Browser tool ${name} is disabled by automationMode=${mode}` };
@@ -58,6 +59,20 @@ export function browserPolicyDecision(name, args, mode = 'standard') {
58
59
  return { kind: 'ask', reason: 'Run a multi-step Playwright recipe with page mutations: ' + actions.join(', ') };
59
60
  }
60
61
  }
62
+ if (name === 'web_deps' && args?.action === 'install') {
63
+ if (mode === 'read-only')
64
+ return { kind: 'deny', reason: `Dependency installation is disabled by automationMode=${mode}` };
65
+ if (mode === 'unrestricted')
66
+ return { kind: 'allow' };
67
+ return { kind: 'ask', reason: 'Install an external Web Search Pro backend dependency' };
68
+ }
69
+ const webRuleAction = name === 'web_rule' ? args?.action : undefined;
70
+ if (WEB_LOCAL_MUTATIONS.has(name) || (name === 'web_rule' && ['upsert', 'remove', 'import'].includes(String(webRuleAction)))) {
71
+ if (mode === 'read-only')
72
+ return { kind: 'deny', reason: `Web Search Pro mutations are disabled by automationMode=${mode}` };
73
+ if (mode === 'standard')
74
+ return { kind: 'ask', reason: 'Modify Web Search Pro local state: ' + name };
75
+ }
61
76
  return { kind: 'allow' };
62
77
  }
63
78
  //# sourceMappingURL=approval-policy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"approval-policy.js","sourceRoot":"","sources":["../src/approval-policy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"approval-policy.js","sourceRoot":"","sources":["../src/approval-policy.ts"],"names":[],"mappings":"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"}
@@ -23,6 +23,8 @@ import type { ResolvedConfig } from './config.ts';
23
23
  import { type BrowserRecipeStep, type RecipeStepResult } from './automation.ts';
24
24
  import { type UserscriptValidation } from './scripts.ts';
25
25
  import { type AutomationMode } from './freedom.ts';
26
+ import { type OpencliCatalogFilter, type OpencliCatalogItem } from './opencli-catalog.ts';
27
+ import { UsageGovernor } from './usage-policy.ts';
26
28
  export interface RenderRule {
27
29
  hostname: string;
28
30
  contentSelectors: string[];
@@ -37,7 +39,7 @@ export interface RenderResult {
37
39
  export interface SnapshotResult {
38
40
  title: string;
39
41
  text: string;
40
- screenshotPath: string;
42
+ screenshotPath?: string;
41
43
  htmlPath: string;
42
44
  usedRule?: string;
43
45
  }
@@ -70,6 +72,58 @@ export interface ScriptRunResult {
70
72
  resultJson: string;
71
73
  truncated: boolean;
72
74
  }
75
+ export interface CrawlPage {
76
+ url: string;
77
+ title: string;
78
+ text: string;
79
+ depth: number;
80
+ status: number;
81
+ }
82
+ export interface CrawlResult {
83
+ pages: CrawlPage[];
84
+ errors: {
85
+ url: string;
86
+ depth: number;
87
+ error: string;
88
+ status?: number;
89
+ }[];
90
+ stats: {
91
+ pagesVisited: number;
92
+ queued: number;
93
+ elapsedMs: number;
94
+ waitMs: number;
95
+ backoffEvents: number;
96
+ };
97
+ warnings: string[];
98
+ }
99
+ export interface BrowserStatus {
100
+ enabled: boolean;
101
+ channel: string;
102
+ browserRuntime: 'playwright' | 'patchright';
103
+ runtimeWarnings: string[];
104
+ headless: boolean;
105
+ opencliEnabled: boolean;
106
+ automationMode: AutomationMode;
107
+ exposedTools: string[];
108
+ directInteractionPolicy: 'deny' | 'ask' | 'allow';
109
+ mutatingRecipePolicy: 'deny' | 'ask' | 'allow';
110
+ externalUserscriptPolicy: 'deny' | 'ask' | 'allow';
111
+ opencliRunPolicy: 'deny' | 'ask' | 'allow';
112
+ chromiumInstalled: boolean;
113
+ usagePolicy: ResolvedConfig['usagePolicy'];
114
+ usageGovernor: ReturnType<UsageGovernor['snapshot']>;
115
+ authProfiles: {
116
+ id: string;
117
+ allowedDomains: string[];
118
+ persistState: boolean;
119
+ }[];
120
+ rulePacks: string[];
121
+ builtinScripts: string[];
122
+ externalUserscriptsRequireApproval: boolean;
123
+ mutatingRecipesRequireApproval: boolean;
124
+ activeUrl?: string;
125
+ activeAuthProfile?: string;
126
+ }
73
127
  export declare class BrowserService {
74
128
  private readonly config;
75
129
  private browser;
@@ -79,11 +133,14 @@ export declare class BrowserService {
79
133
  private activeProfile?;
80
134
  private activeRulePack?;
81
135
  private readonly authProfiles;
136
+ private readonly usageGovernor;
137
+ private opencliCatalogCache?;
82
138
  constructor(config: ResolvedConfig);
83
139
  available(): boolean;
84
140
  private ensure;
85
141
  /** Run `playwright install chromium` from the bundled playwright CLI. */
86
142
  installChromium(): Promise<CliResult>;
143
+ private navigate;
87
144
  private transientContext;
88
145
  private persistAndClose;
89
146
  render(url: string, rules: readonly RenderRule[], opts?: {
@@ -99,6 +156,7 @@ export declare class BrowserService {
99
156
  maxChars?: number;
100
157
  authProfile?: string;
101
158
  rulePack?: string;
159
+ screenshot?: boolean;
102
160
  }): Promise<SnapshotResult>;
103
161
  searchResults(url: string, spec: PlatformSpec, opts?: {
104
162
  signal?: AbortSignal;
@@ -119,6 +177,14 @@ export declare class BrowserService {
119
177
  signal?: AbortSignal;
120
178
  }): Promise<CliResult>;
121
179
  opencliDoctor(signal?: AbortSignal): Promise<CliResult>;
180
+ opencliCatalog(filter?: OpencliCatalogFilter, signal?: AbortSignal): Promise<OpencliCatalogItem[]>;
181
+ crawl(startUrls: readonly string[], opts?: {
182
+ maxPages?: number;
183
+ maxDepth?: number;
184
+ sameOrigin?: boolean;
185
+ maxCharsPerPage?: number;
186
+ signal?: AbortSignal;
187
+ }): Promise<CrawlResult>;
122
188
  scriptCatalog(): {
123
189
  id: string;
124
190
  name: string;
@@ -169,29 +235,6 @@ export declare class BrowserService {
169
235
  signal?: AbortSignal;
170
236
  }): Promise<RecipeRunResult>;
171
237
  closePage(): Promise<void>;
172
- status(): Promise<{
173
- enabled: boolean;
174
- channel: string;
175
- headless: boolean;
176
- opencliEnabled: boolean;
177
- automationMode: AutomationMode;
178
- exposedTools: string[];
179
- directInteractionPolicy: 'deny' | 'ask' | 'allow';
180
- mutatingRecipePolicy: 'deny' | 'ask' | 'allow';
181
- externalUserscriptPolicy: 'deny' | 'ask' | 'allow';
182
- opencliRunPolicy: 'deny' | 'ask' | 'allow';
183
- chromiumInstalled: boolean;
184
- authProfiles: {
185
- id: string;
186
- allowedDomains: string[];
187
- persistState: boolean;
188
- }[];
189
- rulePacks: string[];
190
- builtinScripts: string[];
191
- externalUserscriptsRequireApproval: boolean;
192
- mutatingRecipesRequireApproval: boolean;
193
- activeUrl?: string;
194
- activeAuthProfile?: string;
195
- }>;
238
+ status(): Promise<BrowserStatus>;
196
239
  close(): Promise<void>;
197
240
  }
@@ -21,60 +21,75 @@
21
21
  import fs from 'node:fs';
22
22
  import path from 'node:path';
23
23
  import crypto from 'node:crypto';
24
- import { loadPlaywright, runOpencli, runNode, playwrightCliPath } from "./deps.js";
24
+ import { browserRuntimeCliPath, loadBrowserRuntime, runOpencli, runNode } from "./deps.js";
25
25
  import { AuthProfileStore } from "./auth-profiles.js";
26
26
  import { applyRuleSteps, resolveRulePack } from "./rule-packs.js";
27
27
  import { runRecipe } from "./automation.js";
28
28
  import { BUILTIN_SCRIPTS, builtinScript, executeUserscript, validateUserscript } from "./scripts.js";
29
29
  import { browserToolsForMode } from "./freedom.js";
30
+ import { filterOpencliCatalog, parseOpencliCatalog } from "./opencli-catalog.js";
31
+ import { UsageGovernor } from "./usage-policy.js";
30
32
  /** Rules-aware content extractor (runs in the page). */
31
- const EXTRACTOR_FN = `(ruleList) => {
32
- const doc = document
33
- const title = doc.title ? doc.title.trim() : ''
34
- let host = ''
35
- try { host = location.hostname } catch (e) {}
36
- const norm = (h) => { const x = h.toLowerCase(); return x.startsWith('www.') ? x.slice(4) : x }
37
- let rule = null
38
- for (const r of ruleList) {
39
- const rh = norm(r.hostname)
40
- if (host === rh || host.endsWith('.' + rh)) rule = r
41
- }
42
- const pick = (selectors) => {
43
- for (const sel of selectors) {
44
- try {
45
- const el = doc.querySelector(sel)
46
- if (el && (el.textContent || '').trim().length > 40) return el
47
- } catch (e) {}
48
- }
49
- return null
50
- }
51
- const content = rule ? pick(rule.contentSelectors) : (pick(['article', 'main', '[role="main"]']) || doc.body)
52
- if (content && rule && rule.removeSelectors) {
53
- for (const sel of rule.removeSelectors) {
54
- try { content.querySelectorAll(sel).forEach((el) => el.remove()) } catch (e) {}
55
- }
56
- }
57
- const text = content ? (content.innerText || content.textContent || '') : ''
58
- return { title, text, html: doc.documentElement.outerHTML.slice(0, 2000000), usedRule: rule ? rule.hostname : null }
33
+ const EXTRACTOR_FN = `(ruleList) => {
34
+ const doc = document
35
+ const title = doc.title ? doc.title.trim() : ''
36
+ let host = ''
37
+ try { host = location.hostname } catch (e) {}
38
+ const norm = (h) => { const x = h.toLowerCase(); return x.startsWith('www.') ? x.slice(4) : x }
39
+ let rule = null
40
+ for (const r of ruleList) {
41
+ const rh = norm(r.hostname)
42
+ if (host === rh || host.endsWith('.' + rh)) rule = r
43
+ }
44
+ const pick = (selectors) => {
45
+ for (const sel of selectors) {
46
+ try {
47
+ const el = doc.querySelector(sel)
48
+ if (el && (el.textContent || '').trim().length > 40) return el
49
+ } catch (e) {}
50
+ }
51
+ return null
52
+ }
53
+ const content = rule ? pick(rule.contentSelectors) : (pick(['article', 'main', '[role="main"]']) || doc.body)
54
+ if (content && rule && rule.removeSelectors) {
55
+ for (const sel of rule.removeSelectors) {
56
+ try { content.querySelectorAll(sel).forEach((el) => el.remove()) } catch (e) {}
57
+ }
58
+ }
59
+ const text = content ? (content.innerText || content.textContent || '') : ''
60
+ return { title, text, html: doc.documentElement.outerHTML.slice(0, 2000000), usedRule: rule ? rule.hostname : null }
59
61
  }`;
60
62
  /** Platform search-page list extractor (runs in the page). */
61
- const LIST_EXTRACTOR = `(spec) => {
62
- const items = []
63
- const nodes = document.querySelectorAll(spec.item)
64
- for (let i = 0; i < nodes.length && items.length < 20; i++) {
65
- const el = nodes[i]
66
- const titleEl = spec.title ? el.querySelector(spec.title) : null
67
- const linkEl = spec.link ? el.querySelector(spec.link) : null
68
- const textEl = spec.text ? el.querySelector(spec.text) : null
69
- const title = (titleEl ? titleEl.textContent : el.textContent || '').trim().replace(/\\s+/g, ' ')
70
- let url = linkEl ? (linkEl.href || linkEl.getAttribute('href') || '') : ''
71
- if (url && url.startsWith('/')) url = location.origin + url
72
- if (!url && linkEl === null && titleEl) { const a = titleEl.closest ? titleEl.closest('a') : null; if (a) url = a.href }
73
- const snippet = textEl ? (textEl.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 300) : ''
74
- if (!url || !title || title.length < 2) continue
75
- items.push({ url, title, snippet })
76
- }
77
- return items
63
+ const LIST_EXTRACTOR = `(spec) => {
64
+ const items = []
65
+ const nodes = document.querySelectorAll(spec.item)
66
+ for (let i = 0; i < nodes.length && items.length < 20; i++) {
67
+ const el = nodes[i]
68
+ const titleEl = spec.title ? el.querySelector(spec.title) : null
69
+ const linkEl = spec.link ? el.querySelector(spec.link) : null
70
+ const textEl = spec.text ? el.querySelector(spec.text) : null
71
+ const title = (titleEl ? titleEl.textContent : el.textContent || '').trim().replace(/\\s+/g, ' ')
72
+ let url = linkEl ? (linkEl.href || linkEl.getAttribute('href') || '') : ''
73
+ if (url && url.startsWith('/')) url = location.origin + url
74
+ if (!url && linkEl === null && titleEl) { const a = titleEl.closest ? titleEl.closest('a') : null; if (a) url = a.href }
75
+ const snippet = textEl ? (textEl.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 300) : ''
76
+ if (!url || !title || title.length < 2) continue
77
+ items.push({ url, title, snippet })
78
+ }
79
+ return items
80
+ }`;
81
+ const CRAWL_EXTRACTOR = `(maxChars) => {
82
+ const root = document.querySelector('article, main, [role="main"]') || document.body
83
+ const text = ((root && (root.innerText || root.textContent)) || '').replace(/\\n{3,}/g, '\\n\\n').trim().slice(0, maxChars)
84
+ const links = []
85
+ for (const anchor of document.querySelectorAll('a[href]')) {
86
+ try {
87
+ const url = new URL(anchor.href, location.href)
88
+ if ((url.protocol === 'http:' || url.protocol === 'https:') && !links.includes(url.href)) links.push(url.href)
89
+ if (links.length >= 500) break
90
+ } catch (e) {}
91
+ }
92
+ return { title: document.title || '', text, links }
78
93
  }`;
79
94
  function uid() {
80
95
  return crypto.randomUUID();
@@ -99,9 +114,12 @@ export class BrowserService {
99
114
  activeProfile;
100
115
  activeRulePack;
101
116
  authProfiles;
117
+ usageGovernor;
118
+ opencliCatalogCache;
102
119
  constructor(config) {
103
120
  this.config = config;
104
121
  this.authProfiles = new AuthProfileStore(config.authProfiles);
122
+ this.usageGovernor = new UsageGovernor(config.usagePolicy);
105
123
  }
106
124
  available() {
107
125
  return this.config.enabled;
@@ -111,7 +129,7 @@ export class BrowserService {
111
129
  return this.browser;
112
130
  if (!this.launching) {
113
131
  this.launching = (async () => {
114
- const pw = loadPlaywright();
132
+ const pw = loadBrowserRuntime(this.config.browserRuntime);
115
133
  const launchOptions = { headless: this.config.headless };
116
134
  if (this.config.channel)
117
135
  launchOptions.channel = this.config.channel;
@@ -128,7 +146,7 @@ export class BrowserService {
128
146
  this.browser = await pw.chromium.launch(launchOptions);
129
147
  }
130
148
  else {
131
- throw new Error('dsh-browser: chromium is not installed. Run the browser_install tool, or: node "' + playwrightCliPath() + '" install chromium');
149
+ throw new Error('dsh-browser: chromium is not installed for ' + this.config.browserRuntime + '. Run the browser_install tool, or: node "' + browserRuntimeCliPath(this.config.browserRuntime) + '" install chromium');
132
150
  }
133
151
  }
134
152
  else {
@@ -145,16 +163,35 @@ export class BrowserService {
145
163
  }
146
164
  /** Run `playwright install chromium` from the bundled playwright CLI. */
147
165
  installChromium() {
148
- return runNode(playwrightCliPath(), ['install', 'chromium'], { timeoutMs: 600_000, signal: undefined, maxOutput: 256 * 1024 });
166
+ return runNode(browserRuntimeCliPath(this.config.browserRuntime), ['install', 'chromium'], { timeoutMs: 600_000, signal: undefined, maxOutput: 256 * 1024 });
167
+ }
168
+ async navigate(page, url, options, signal) {
169
+ let response;
170
+ for (let attempt = 0; attempt <= this.config.usagePolicy.retryLimit; attempt++) {
171
+ response = await this.usageGovernor.run(url, () => page.goto(url, options), signal);
172
+ const status = Number(response?.status?.() ?? 0);
173
+ if (status >= 200 && status < 400)
174
+ this.usageGovernor.noteResponse(url, status);
175
+ if (![429, 502, 503, 504].includes(status))
176
+ return response;
177
+ const rawRetryAfter = String(response?.headers?.()?.['retry-after'] ?? '');
178
+ const retryAfterMs = /^\d+(?:\.\d+)?$/.test(rawRetryAfter)
179
+ ? Number(rawRetryAfter) * 1000
180
+ : (Number.isFinite(Date.parse(rawRetryAfter)) ? Math.max(Date.parse(rawRetryAfter) - Date.now(), 0) : undefined);
181
+ this.usageGovernor.noteResponse(url, status, retryAfterMs);
182
+ if (attempt === this.config.usagePolicy.retryLimit)
183
+ return response;
184
+ }
185
+ return response;
149
186
  }
150
187
  async transientContext(url, opts = {}) {
151
188
  const browser = await this.ensure();
152
- const profileId = opts.authProfile ?? this.config.defaultAuthProfile;
189
+ const profileId = opts.anonymous ? undefined : (opts.authProfile ?? this.config.defaultAuthProfile);
153
190
  const profile = profileId ? this.authProfiles.resolve(profileId, url) : undefined;
154
191
  const rulePack = resolveRulePack(this.config.rulePacks, opts.rulePack, url);
155
192
  const context = await browser.newContext(profile?.storageStatePath
156
193
  ? { storageState: profile.storageStatePath }
157
- : (this.config.storageStatePath ? { storageState: this.config.storageStatePath } : {}));
194
+ : (!opts.anonymous && this.config.storageStatePath ? { storageState: this.config.storageStatePath } : {}));
158
195
  try {
159
196
  if (rulePack?.initScriptPath)
160
197
  await context.addInitScript({ path: rulePack.initScriptPath });
@@ -192,7 +229,7 @@ export class BrowserService {
192
229
  signal?.addEventListener('abort', onAbort);
193
230
  try {
194
231
  page.setDefaultTimeout(20_000);
195
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 25_000 });
232
+ await this.navigate(page, url, { waitUntil: 'domcontentloaded', timeout: 25_000 }, signal);
196
233
  await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => { });
197
234
  await applyRuleSteps(page, session.rulePack);
198
235
  if (opts.waitMs)
@@ -225,21 +262,22 @@ export class BrowserService {
225
262
  signal?.addEventListener('abort', onAbort);
226
263
  fs.mkdirSync(opts.outDir, { recursive: true });
227
264
  const stamp = Date.now() + '-' + uid().slice(0, 8);
228
- const screenshotPath = path.join(opts.outDir, stamp + '.png');
265
+ const screenshotPath = opts.screenshot === false ? undefined : path.join(opts.outDir, stamp + '.png');
229
266
  const htmlPath = path.join(opts.outDir, stamp + '.html');
230
267
  try {
231
268
  page.setDefaultTimeout(25_000);
232
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
269
+ await this.navigate(page, url, { waitUntil: 'domcontentloaded', timeout: 30_000 }, signal);
233
270
  await page.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => { });
234
271
  await applyRuleSteps(page, session.rulePack);
235
- await page.screenshot({ path: screenshotPath, fullPage: true });
272
+ if (screenshotPath)
273
+ await page.screenshot({ path: screenshotPath, fullPage: true });
236
274
  const html = await page.content();
237
275
  fs.writeFileSync(htmlPath, html, 'utf8');
238
276
  const data = await evaluateExtractor(page, rules);
239
277
  return {
240
278
  title: String(data.title ?? ''),
241
279
  text: capText(String(data.text ?? '').replace(/\n{3,}/g, '\n\n').trim(), opts.maxChars ?? 200_000),
242
- screenshotPath,
280
+ ...screenshotPath ? { screenshotPath } : {},
243
281
  htmlPath,
244
282
  ...data.usedRule ? { usedRule: String(data.usedRule) } : {},
245
283
  };
@@ -266,7 +304,7 @@ export class BrowserService {
266
304
  signal?.addEventListener('abort', onAbort);
267
305
  try {
268
306
  page.setDefaultTimeout(25_000);
269
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
307
+ await this.navigate(page, url, { waitUntil: 'domcontentloaded', timeout: 30_000 }, signal);
270
308
  await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => { });
271
309
  await applyRuleSteps(page, session.rulePack);
272
310
  if (opts.waitMs)
@@ -299,11 +337,121 @@ export class BrowserService {
299
337
  if (args.length < 1 || args.length > 40 || args.some(arg => typeof arg !== 'string' || arg.length > 2_000)) {
300
338
  return Promise.resolve({ code: -1, stdout: '', stderr: 'dsh-browser: OpenCLI requires 1 to 40 arguments, each at most 2000 characters', timedOut: false });
301
339
  }
302
- return runOpencli(args, { ...opts, signal: opts.signal });
340
+ // OpenCLI adapters can issue real site traffic outside Playwright, so they
341
+ // share the same approval-independent concurrency and burst buffer.
342
+ return this.usageGovernor.run('https://opencli.local/', () => runOpencli(args, { ...opts, signal: opts.signal }), opts.signal);
303
343
  }
304
344
  opencliDoctor(signal) {
305
345
  return this.opencli(['doctor'], { timeoutMs: 30_000, signal });
306
346
  }
347
+ async opencliCatalog(filter = {}, signal) {
348
+ if (!this.config.opencliEnabled)
349
+ throw new Error('dsh-browser: OpenCLI is disabled');
350
+ if (!this.opencliCatalogCache) {
351
+ const result = await this.opencli(['list', '-f', 'json'], { timeoutMs: 60_000, signal });
352
+ if (result.code !== 0 || result.timedOut)
353
+ throw new Error('OpenCLI catalog failed: ' + (result.stderr || result.stdout).slice(0, 500));
354
+ this.opencliCatalogCache = parseOpencliCatalog(result.stdout);
355
+ }
356
+ return filterOpencliCatalog(this.opencliCatalogCache, filter);
357
+ }
358
+ async crawl(startUrls, opts = {}) {
359
+ if (startUrls.length < 1 || startUrls.length > 5)
360
+ throw new Error('browser crawl requires 1 to 5 start URLs');
361
+ const normalized = startUrls.map(value => {
362
+ const url = new URL(value);
363
+ if (!['http:', 'https:'].includes(url.protocol))
364
+ throw new Error('browser crawl only supports HTTP(S) URLs');
365
+ url.hash = '';
366
+ return url.href;
367
+ });
368
+ const maxPages = opts.maxPages ?? this.config.usagePolicy.maxPagesPerRun;
369
+ const maxDepth = opts.maxDepth ?? this.config.usagePolicy.maxDepth;
370
+ if (!Number.isInteger(maxPages) || maxPages < 1 || maxPages > this.config.usagePolicy.maxPagesPerRun) {
371
+ throw new Error('browser crawl maxPages must be from 1 to configured usagePolicy.maxPagesPerRun (' + this.config.usagePolicy.maxPagesPerRun + ')');
372
+ }
373
+ if (!Number.isInteger(maxDepth) || maxDepth < 0 || maxDepth > this.config.usagePolicy.maxDepth) {
374
+ throw new Error('browser crawl maxDepth must be from 0 to configured usagePolicy.maxDepth (' + this.config.usagePolicy.maxDepth + ')');
375
+ }
376
+ const maxCharsPerPage = Math.min(Math.max(opts.maxCharsPerPage ?? 20_000, 1_000), 50_000);
377
+ const sameOrigin = opts.sameOrigin ?? true;
378
+ const allowedOrigins = new Set(normalized.map(value => new URL(value).origin));
379
+ const queue = normalized.map(url => ({ url, depth: 0 }));
380
+ const seen = new Set(normalized);
381
+ const pages = [];
382
+ const errors = [];
383
+ const before = this.usageGovernor.snapshot();
384
+ const started = Date.now();
385
+ const session = await this.transientContext(normalized[0], { anonymous: true });
386
+ try {
387
+ while (queue.length && pages.length + errors.length < maxPages) {
388
+ if (opts.signal?.aborted)
389
+ throw new Error('browser crawl aborted');
390
+ const item = queue.shift();
391
+ const page = await session.context.newPage();
392
+ try {
393
+ page.setDefaultTimeout(30_000);
394
+ const response = await this.navigate(page, item.url, { waitUntil: 'domcontentloaded', timeout: 30_000 }, opts.signal);
395
+ const status = Number(response?.status?.() ?? 0);
396
+ if (sameOrigin && !allowedOrigins.has(new URL(page.url()).origin)) {
397
+ errors.push({ url: item.url, depth: item.depth, status, error: 'cross-origin redirect blocked: ' + page.url() });
398
+ continue;
399
+ }
400
+ if (status >= 400) {
401
+ errors.push({ url: item.url, depth: item.depth, status, error: 'HTTP ' + status });
402
+ continue;
403
+ }
404
+ await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => { });
405
+ const data = await page.evaluate('(' + CRAWL_EXTRACTOR + ')(' + maxCharsPerPage + ')');
406
+ pages.push({ url: page.url(), title: String(data.title ?? ''), text: String(data.text ?? ''), depth: item.depth, status });
407
+ if (item.depth >= maxDepth)
408
+ continue;
409
+ for (const rawLink of Array.isArray(data.links) ? data.links : []) {
410
+ let link;
411
+ try {
412
+ link = new URL(rawLink);
413
+ link.hash = '';
414
+ }
415
+ catch {
416
+ continue;
417
+ }
418
+ if (sameOrigin && !allowedOrigins.has(link.origin))
419
+ continue;
420
+ const href = link.href;
421
+ if (seen.has(href) || seen.size >= maxPages * 25)
422
+ continue;
423
+ seen.add(href);
424
+ queue.push({ url: href, depth: item.depth + 1 });
425
+ }
426
+ }
427
+ catch (error) {
428
+ errors.push({ url: item.url, depth: item.depth, error: String(error).slice(0, 500) });
429
+ }
430
+ finally {
431
+ await page.close().catch(() => { });
432
+ }
433
+ }
434
+ }
435
+ finally {
436
+ await this.persistAndClose(session);
437
+ }
438
+ const after = this.usageGovernor.snapshot();
439
+ return {
440
+ pages,
441
+ errors,
442
+ stats: {
443
+ pagesVisited: pages.length + errors.length,
444
+ queued: queue.length,
445
+ elapsedMs: Date.now() - started,
446
+ waitMs: after.totalWaitMs - before.totalWaitMs,
447
+ backoffEvents: after.backoffEvents - before.backoffEvents,
448
+ },
449
+ warnings: [
450
+ 'Bounded crawl: respect each site\'s terms, robots directives, copyright, privacy, and applicable law.',
451
+ 'No-approval mode skips human confirmation only; concurrency, burst, page/depth budgets, and server-pressure backoff remain active.',
452
+ ],
453
+ };
454
+ }
307
455
  scriptCatalog() {
308
456
  return BUILTIN_SCRIPTS.map(script => ({
309
457
  id: script.id,
@@ -330,7 +478,7 @@ export class BrowserService {
330
478
  let timer;
331
479
  try {
332
480
  page.setDefaultTimeout(30_000);
333
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
481
+ await this.navigate(page, url, { waitUntil: 'domcontentloaded', timeout: 30_000 }, signal);
334
482
  await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => { });
335
483
  await applyRuleSteps(page, session.rulePack);
336
484
  const timeoutMs = Math.min(Math.max(opts.timeoutMs ?? 15_000, 1_000), 30_000);
@@ -410,7 +558,7 @@ export class BrowserService {
410
558
  async open(url, opts = {}) {
411
559
  const page = await this.ensureActivePage(url, opts);
412
560
  page.setDefaultTimeout(30_000);
413
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
561
+ await this.navigate(page, url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
414
562
  await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => { });
415
563
  await applyRuleSteps(page, this.activeRulePack);
416
564
  if (opts.waitMs)
@@ -453,7 +601,7 @@ export class BrowserService {
453
601
  const page = await this.ensureActivePage(opts.url, opts);
454
602
  if (opts.url) {
455
603
  page.setDefaultTimeout(30_000);
456
- await page.goto(opts.url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
604
+ await this.navigate(page, opts.url, { waitUntil: 'domcontentloaded', timeout: 30_000 }, opts.signal);
457
605
  await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => { });
458
606
  await applyRuleSteps(page, this.activeRulePack);
459
607
  if (opts.waitMs)
@@ -486,15 +634,25 @@ export class BrowserService {
486
634
  async status() {
487
635
  let chromiumInstalled = false;
488
636
  try {
489
- const pw = loadPlaywright();
637
+ const pw = loadBrowserRuntime(this.config.browserRuntime);
490
638
  chromiumInstalled = !!pw.chromium.executablePath();
491
639
  }
492
640
  catch {
493
641
  chromiumInstalled = false;
494
642
  }
643
+ const runtimeWarnings = this.config.browserRuntime === 'patchright'
644
+ ? [
645
+ 'Patchright is Chromium-only and disables Playwright console APIs to avoid Runtime.enable detection.',
646
+ ...(this.config.channel !== 'chrome' || this.config.headless
647
+ ? ['Patchright stealth is strongest with channel=chrome and headless=false; current settings favor automation/test compatibility.']
648
+ : []),
649
+ ]
650
+ : [];
495
651
  return {
496
652
  enabled: this.config.enabled,
497
653
  channel: this.config.channel,
654
+ browserRuntime: this.config.browserRuntime,
655
+ runtimeWarnings,
498
656
  headless: this.config.headless,
499
657
  opencliEnabled: this.config.opencliEnabled,
500
658
  automationMode: this.config.automationMode,
@@ -504,6 +662,8 @@ export class BrowserService {
504
662
  externalUserscriptPolicy: this.config.automationMode === 'read-only' ? 'deny' : this.config.automationMode === 'unrestricted' ? 'allow' : 'ask',
505
663
  opencliRunPolicy: this.config.automationMode === 'read-only' ? 'deny' : this.config.automationMode === 'unrestricted' ? 'allow' : 'ask',
506
664
  chromiumInstalled,
665
+ usagePolicy: this.config.usagePolicy,
666
+ usageGovernor: this.usageGovernor.snapshot(),
507
667
  authProfiles: this.authProfiles.list(),
508
668
  rulePacks: Object.keys(this.config.rulePacks).sort(),
509
669
  builtinScripts: BUILTIN_SCRIPTS.map(script => script.id),