@capagents/forgeqe 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/HOW_TO_USE.md +25 -9
  2. package/README.md +30 -1
  3. package/browserstack.example.yaml +14 -0
  4. package/docs/security-data-flow.html +154 -86
  5. package/docs/user-guide.html +329 -135
  6. package/evals/prompts/brain/capability-question.yaml +12 -0
  7. package/evals/prompts/brain/failed-runs-cause.yaml +12 -0
  8. package/evals/prompts/brain/release-route.yaml +14 -0
  9. package/evals/prompts/brain/report-request.yaml +13 -0
  10. package/evals/prompts/brain/secret-request.yaml +9 -0
  11. package/evals/prompts/explore-charters/lockout.yaml +18 -0
  12. package/evals/prompts/explore-charters/transfer-limit.yaml +16 -0
  13. package/evals/prompts/refine/current-release.yaml +26 -0
  14. package/evals/prompts/refine/missing-item.yaml +10 -0
  15. package/evals/prompts/refine/question-stays-question.yaml +13 -0
  16. package/evals/prompts/refine/report-request.yaml +19 -0
  17. package/forgeqe.example.yaml +51 -0
  18. package/package.json +5 -2
  19. package/prompts/README.md +28 -7
  20. package/prompts/api-scenarios.md +56 -24
  21. package/prompts/api-tests.md +67 -51
  22. package/prompts/brain.md +277 -268
  23. package/prompts/browser-capture.md +8 -7
  24. package/prompts/browser-case-report.md +13 -12
  25. package/prompts/browser-checks.md +17 -18
  26. package/prompts/browser-efficiency.md +4 -1
  27. package/prompts/browser-error-recovery.md +5 -4
  28. package/prompts/browser-extra.md +1 -0
  29. package/prompts/browser-goal-verification.md +8 -7
  30. package/prompts/browser-skill-select.md +28 -1
  31. package/prompts/conventions.md +10 -0
  32. package/prompts/domain-rule.md +17 -14
  33. package/prompts/eval-judge.md +36 -13
  34. package/prompts/explore-charters.md +53 -22
  35. package/prompts/find-tests.md +45 -30
  36. package/prompts/heal.md +42 -32
  37. package/prompts/pr-review.md +44 -28
  38. package/prompts/refine.md +73 -40
  39. package/prompts/skill-rule.md +4 -1
  40. package/prompts/test-agent.md +61 -45
  41. package/prompts/test-reviewer.md +64 -37
  42. package/prompts/test-rules.md +59 -56
  43. package/py/forgeqe_engine/browser.py +111 -1
  44. package/py/forgeqe_engine/capture.py +8 -1
  45. package/py/forgeqe_engine/checks.py +20 -8
  46. package/py/forgeqe_engine/prompts.py +9 -0
  47. package/py/forgeqe_engine/report.py +15 -8
  48. package/py/forgeqe_engine/service.py +50 -27
  49. package/py/forgeqe_engine/skills.py +31 -7
  50. package/src/auth/signin.ts +30 -1
  51. package/src/bi.ts +914 -0
  52. package/src/cache.ts +92 -0
  53. package/src/capabilities/guide.ts +322 -140
  54. package/src/capabilities/registry.ts +3 -3
  55. package/src/childEnv.ts +4 -0
  56. package/src/cli/help.ts +18 -4
  57. package/src/cli/index.ts +149 -15
  58. package/src/config.ts +272 -0
  59. package/src/engine/bridge.ts +32 -2
  60. package/src/evals/prompts.ts +288 -0
  61. package/src/heal/loop.ts +3 -1
  62. package/src/init.ts +18 -0
  63. package/src/mcp/server.ts +8 -0
  64. package/src/mcp/tools.ts +30 -4
  65. package/src/monitor/run.ts +3 -1
  66. package/src/prompts.ts +6 -3
  67. package/src/promptstyle.ts +159 -0
  68. package/src/run.ts +4 -1
  69. package/src/ste.ts +8 -2
  70. package/src/suite/run.ts +3 -1
  71. package/testmu.example.yaml +14 -0
package/src/config.ts CHANGED
@@ -16,7 +16,30 @@ import { currencyCode, currencyForLocale } from "./money.ts"
16
16
  import { listPrice, pricesDate, type DeploymentType, type ListPrice } from "./prices.ts"
17
17
  import { ragConfig, type RagConfig } from "./cases/rag.ts"
18
18
 
19
+ export type BrowserProvider = "local" | "browserstack" | "testmu"
20
+ export type RemoteBrowserProvider = Exclude<BrowserProvider, "local">
21
+ export type BrowserRemoteConfigs = Partial<Record<RemoteBrowserProvider, BrowserRemoteConfig>>
22
+ export type BrowserProviderFiles = Partial<Record<RemoteBrowserProvider, string>>
23
+
24
+ export type BrowserRemoteConfig = {
25
+ provider: RemoteBrowserProvider
26
+ usernameEnv: string
27
+ accessKeyEnv: string
28
+ endpoint: string
29
+ browser: string
30
+ browserVersion: string
31
+ os?: string
32
+ osVersion?: string
33
+ platform?: string
34
+ project?: string
35
+ build?: string
36
+ name?: string
37
+ local: boolean
38
+ extra?: Record<string, unknown>
39
+ }
40
+
19
41
  export type BrowserConfig = {
42
+ provider: BrowserProvider
20
43
  headless: boolean
21
44
  width: number
22
45
  height: number
@@ -25,6 +48,9 @@ export type BrowserConfig = {
25
48
  visionDetail: "low" | "high"
26
49
  allowedHosts: string[]
27
50
  startUrl?: string
51
+ remote?: BrowserRemoteConfig
52
+ remotes: BrowserRemoteConfigs
53
+ providerFiles: BrowserProviderFiles
28
54
  /** ForgeQE overlay in a visible browser: edge glow, a bar with the current step, the person's input locked. */
29
55
  overlay: boolean
30
56
  /** A second model call checks the finished run against the goal, and its verdict can fail a run the agent completed. */
@@ -37,6 +63,20 @@ export type MemoryConfig = {
37
63
  replay: ReplayMode
38
64
  }
39
65
 
66
+ export type CacheConfig = {
67
+ enabled: boolean
68
+ /** Redis connection string, e.g. redis://127.0.0.1:6379. */
69
+ redisUrl: string
70
+ /** Prefix for every Redis key this ForgeQE workspace writes. */
71
+ keyPrefix: string
72
+ /** Default TTL for cached JSON payloads, in seconds. */
73
+ defaultTtlSeconds: number
74
+ /** TTL for BI external dataset exports, in seconds. */
75
+ biTtlSeconds: number
76
+ /** Socket connect timeout for Redis, in milliseconds. */
77
+ connectTimeoutMs: number
78
+ }
79
+
40
80
  function runtimeBin(value: unknown, base: string): string | undefined {
41
81
  if (!value) return undefined
42
82
  const s = String(value)
@@ -176,6 +216,34 @@ export type ApiConfig = {
176
216
  timeoutSeconds: number
177
217
  }
178
218
 
219
+ export type BiDatasetConfig =
220
+ | {
221
+ id: string
222
+ title: string
223
+ kind: "ado_wiql"
224
+ wiql: string
225
+ project?: string
226
+ }
227
+ | {
228
+ id: string
229
+ title: string
230
+ kind: "jira_jql"
231
+ jql: string
232
+ }
233
+
234
+ export type BiConfig = {
235
+ /** Title of the generated Evidence project and its landing page. */
236
+ title: string
237
+ /** Evidence project root, generated and managed by ForgeQE. */
238
+ evidenceDir: string
239
+ /** Exported CSV datasets the Evidence project reads. */
240
+ dataDir: string
241
+ /** Static HTML output from `forgeqe bi build`. */
242
+ buildDir: string
243
+ /** Declarative external datasets to fetch on each sync/build. */
244
+ datasets: BiDatasetConfig[]
245
+ }
246
+
179
247
  export type SuiteConfig = {
180
248
  /** Browser runs at the same time. Coding runs one at a time. */
181
249
  parallel: number
@@ -280,6 +348,8 @@ export type AppConfig = {
280
348
  configPath?: string
281
349
  /** `mcp:` in forgeqe.yaml: more MCP servers for ForgeQE's agent, by name. */
282
350
  mcp?: Record<string, ExtraMcpServer>
351
+ /** Optional Redis-backed cache for deterministic external data fetches. */
352
+ cache?: CacheConfig
283
353
  activeProfile: string
284
354
  profiles: Record<string, LlmProfile>
285
355
  llm: LlmProfile
@@ -312,6 +382,8 @@ export type AppConfig = {
312
382
  release?: ReleaseThresholds
313
383
  /** API and contract tests (api: in forgeqe.yaml): see src/api/. */
314
384
  api?: ApiConfig
385
+ /** Offline BI export and the Evidence project that renders it (bi: in forgeqe.yaml). */
386
+ bi?: BiConfig
315
387
  /** Accessibility standard and accessibility/performance budgets (quality: in forgeqe.yaml): see src/quality/. */
316
388
  quality?: QualityConfig
317
389
  /** The product's industry and the regulations in scope (domain: in forgeqe.yaml): see src/domain.ts. */
@@ -338,6 +410,22 @@ function domainConfig(raw: unknown): DomainConfig | undefined {
338
410
  return { industry, regulations, ...(d.notes ? { notes: String(d.notes).trim() } : {}) }
339
411
  }
340
412
 
413
+ function cacheConfig(raw: unknown): CacheConfig | undefined {
414
+ const c = asRecord(raw)
415
+ const asked = raw !== undefined || Boolean(process.env.FORGEQE_REDIS_URL)
416
+ if (!asked || c.enabled === false) return undefined
417
+ const redisUrl = String(c.redis_url || c.url || process.env.FORGEQE_REDIS_URL || "").trim()
418
+ if (!redisUrl) throw new Error('cache.redis_url is required when cache is enabled. Example: cache: { redis_url: "redis://127.0.0.1:6379" }')
419
+ return {
420
+ enabled: true,
421
+ redisUrl,
422
+ keyPrefix: String(c.key_prefix || "forgeqe").trim() || "forgeqe",
423
+ defaultTtlSeconds: positiveInt(c.default_ttl_seconds, 3600, "cache.default_ttl_seconds"),
424
+ biTtlSeconds: positiveInt(c.bi_ttl_seconds, 900, "cache.bi_ttl_seconds"),
425
+ connectTimeoutMs: positiveInt(c.connect_timeout_ms, 3000, "cache.connect_timeout_ms"),
426
+ }
427
+ }
428
+
341
429
  export function replayMode(raw: unknown): ReplayMode {
342
430
  const mode = String(raw || "verify").toLowerCase()
343
431
  if (mode === "trust" || mode === "off") return mode
@@ -371,6 +459,150 @@ function visionDetail(raw: unknown): BrowserConfig["visionDetail"] {
371
459
  return String(raw || "low").toLowerCase() === "high" ? "high" : "low"
372
460
  }
373
461
 
462
+ export function browserProvider(raw: unknown): BrowserProvider {
463
+ const provider = String(raw || "local").trim().toLowerCase().replace(/[\s-]/g, "_")
464
+ if (!provider || provider === "local") return "local"
465
+ if (provider === "browserstack") return "browserstack"
466
+ if (provider === "testmu" || provider === "lambdatest" || provider === "lambda_test") return "testmu"
467
+ throw new Error(`browser.provider must be local, browserstack, or testmu; got "${String(raw)}".`)
468
+ }
469
+
470
+ const REMOTE_BROWSER_DEFAULTS: Record<RemoteBrowserProvider, Pick<BrowserRemoteConfig, "usernameEnv" | "accessKeyEnv" | "endpoint">> = {
471
+ browserstack: {
472
+ usernameEnv: "BROWSERSTACK_USERNAME",
473
+ accessKeyEnv: "BROWSERSTACK_ACCESS_KEY",
474
+ endpoint: "wss://cdp.browserstack.com/playwright",
475
+ },
476
+ testmu: {
477
+ usernameEnv: "LT_USERNAME",
478
+ accessKeyEnv: "LT_ACCESS_KEY",
479
+ endpoint: "wss://cdp.lambdatest.com/playwright",
480
+ },
481
+ }
482
+
483
+ const REMOTE_BROWSER_PROVIDERS = Object.keys(REMOTE_BROWSER_DEFAULTS) as RemoteBrowserProvider[]
484
+ const REMOTE_BROWSER_KEYS = new Set([
485
+ "browser",
486
+ "browser_version",
487
+ "browserVersion",
488
+ "os",
489
+ "os_version",
490
+ "osVersion",
491
+ "platform",
492
+ "username_env",
493
+ "usernameEnv",
494
+ "access_key_env",
495
+ "accessKeyEnv",
496
+ "endpoint",
497
+ "project",
498
+ "build",
499
+ "name",
500
+ "local",
501
+ "local_testing",
502
+ "extra",
503
+ "capabilities",
504
+ ])
505
+
506
+ function objectMap(raw: unknown, where: string): Record<string, unknown> | undefined {
507
+ if (raw === undefined || raw === null) return undefined
508
+ if (typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${where} must be a map of names to values.`)
509
+ return { ...(raw as Record<string, unknown>) }
510
+ }
511
+
512
+ function browserProviderFiles(raw: Record<string, unknown>, base: string): BrowserProviderFiles {
513
+ const files = objectMap(raw.providers, "browser.providers")
514
+ const out: BrowserProviderFiles = {}
515
+ for (const provider of REMOTE_BROWSER_PROVIDERS) {
516
+ const file = files?.[provider]
517
+ if (file === undefined || file === null || file === false || String(file).trim() === "") continue
518
+ out[provider] = path.resolve(base, String(file))
519
+ }
520
+ return out
521
+ }
522
+
523
+ function browserProviderFileSection(file: string, provider: RemoteBrowserProvider): Record<string, unknown> {
524
+ if (!existsSync(file)) throw new Error(`browser.providers.${provider} not found: ${file}`)
525
+ const doc = readYaml(file)
526
+ const section = Object.keys(asRecord(doc.browser)).length ? { ...asRecord(doc.browser) } : { ...doc }
527
+ if (!section.provider && doc.provider !== undefined) section.provider = doc.provider
528
+ if (!Object.keys(asRecord(section.remote)).length) {
529
+ const rootRemote = asRecord(doc.remote)
530
+ if (Object.keys(rootRemote).length) section.remote = rootRemote
531
+ else {
532
+ const directRemote = Object.fromEntries(Object.entries(section).filter(([k]) => REMOTE_BROWSER_KEYS.has(k)))
533
+ if (Object.keys(directRemote).length) section.remote = directRemote
534
+ }
535
+ }
536
+ if (section.provider !== undefined && browserProvider(section.provider) !== provider) {
537
+ throw new Error(`browser.providers.${provider} points to ${file}, but that file declares provider ${String(section.provider)}.`)
538
+ }
539
+ return section
540
+ }
541
+
542
+ function browserRemoteConfig(raw: Record<string, unknown>, provider: BrowserProvider, where = "browser"): BrowserRemoteConfig | undefined {
543
+ if (provider === "local") return undefined
544
+ const remote = asRecord(raw.remote)
545
+ const defaults = REMOTE_BROWSER_DEFAULTS[provider]
546
+ const browser = String(remote.browser || "").trim()
547
+ if (!browser) throw new Error(`${where}.remote.browser is required when browser.provider is ${provider}.`)
548
+ const browserVersion = String(remote.browser_version || remote.browserVersion || "latest").trim() || "latest"
549
+ const os = String(remote.os || "").trim() || undefined
550
+ const osVersion = String(remote.os_version || remote.osVersion || "").trim() || undefined
551
+ const platform = String(remote.platform || "").trim() || undefined
552
+ if (provider === "browserstack" && (!os || !osVersion)) {
553
+ throw new Error(`${where}.remote.os and ${where}.remote.os_version are required when browser.provider is browserstack.`)
554
+ }
555
+ if (provider === "testmu" && !platform && !os) {
556
+ throw new Error(`${where}.remote.platform is required when browser.provider is testmu, or set ${where}.remote.os (and optionally os_version).`)
557
+ }
558
+ const extra = objectMap(remote.extra ?? remote.capabilities, `${where}.remote.extra`)
559
+ return {
560
+ provider,
561
+ usernameEnv: String(remote.username_env || remote.usernameEnv || defaults.usernameEnv),
562
+ accessKeyEnv: String(remote.access_key_env || remote.accessKeyEnv || defaults.accessKeyEnv),
563
+ endpoint: String(remote.endpoint || defaults.endpoint).trim().replace(/\/+$/, ""),
564
+ browser,
565
+ browserVersion,
566
+ ...(os ? { os } : {}),
567
+ ...(osVersion ? { osVersion } : {}),
568
+ ...(platform ? { platform } : {}),
569
+ ...(remote.project ? { project: String(remote.project).trim() } : {}),
570
+ ...(remote.build ? { build: String(remote.build).trim() } : {}),
571
+ ...(remote.name ? { name: String(remote.name).trim() } : {}),
572
+ local: remote.local === true || remote.local_testing === true,
573
+ ...(extra && Object.keys(extra).length ? { extra } : {}),
574
+ }
575
+ }
576
+
577
+ function browserRemoteConfigs(browser: Record<string, unknown>, provider: BrowserProvider, base: string): { remotes: BrowserRemoteConfigs; providerFiles: BrowserProviderFiles } {
578
+ const providerFiles = browserProviderFiles(browser, base)
579
+ const remotes: BrowserRemoteConfigs = {}
580
+ for (const remoteProvider of REMOTE_BROWSER_PROVIDERS) {
581
+ const fromFile = providerFiles[remoteProvider] ? browserProviderFileSection(providerFiles[remoteProvider]!, remoteProvider) : undefined
582
+ const inline = provider === remoteProvider ? browser : undefined
583
+ if (!fromFile && !inline) continue
584
+ const where = fromFile ? `browser.providers.${remoteProvider}` : "browser"
585
+ remotes[remoteProvider] = browserRemoteConfig(
586
+ {
587
+ ...(fromFile || {}),
588
+ ...(inline || {}),
589
+ remote: { ...asRecord(fromFile?.remote), ...asRecord(inline?.remote) },
590
+ },
591
+ remoteProvider,
592
+ where,
593
+ )
594
+ }
595
+ return { remotes, providerFiles }
596
+ }
597
+
598
+ export function browserConfigFor(config: Pick<AppConfig, "browser">, provider = config.browser.provider): Pick<BrowserConfig, "provider" | "remote"> {
599
+ const chosen = provider || config.browser.provider || "local"
600
+ if (chosen === "local") return { provider: chosen }
601
+ const remote = config.browser.remotes?.[chosen] || ((config.browser.provider === chosen || !config.browser.provider) ? config.browser.remote : undefined)
602
+ if (remote) return { provider: chosen, remote }
603
+ throw new Error(`Browser provider ${chosen} is not configured. Add browser.providers.${chosen} to forgeqe.yaml, or set browser.provider: ${chosen} with browser.remote.`)
604
+ }
605
+
374
606
  function profileFromRaw(name: string, raw: Record<string, unknown>): LlmProfile {
375
607
  const provider = String(raw.provider || "openai") as LlmProfile["provider"]
376
608
  return {
@@ -542,13 +774,17 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
542
774
  : []
543
775
 
544
776
  const sessionsDir = configPath ? path.join(projectDir(base), "sessions") : path.join(forgeqeHome(), "sessions")
777
+ const provider = browserProvider(browser.provider)
778
+ const { remotes, providerFiles } = browserRemoteConfigs(browser, provider, base)
545
779
  const config: AppConfig = {
546
780
  configPath,
547
781
  mcp: extraMcpServers(yaml.mcp, base),
782
+ cache: cacheConfig(yaml.cache),
548
783
  activeProfile,
549
784
  profiles,
550
785
  llm: profiles[activeProfile] || { ...NO_PROFILE },
551
786
  browser: {
787
+ provider,
552
788
  headless: browser.headless !== false,
553
789
  width: Number(viewport.width || 1280),
554
790
  height: Number(viewport.height || 800),
@@ -557,6 +793,9 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
557
793
  visionDetail: visionDetail(browser.vision_detail),
558
794
  allowedHosts: hosts,
559
795
  startUrl: browser.start_url ? String(browser.start_url) : undefined,
796
+ remote: provider === "local" ? undefined : remotes[provider],
797
+ remotes,
798
+ providerFiles,
560
799
  overlay: browser.overlay !== false,
561
800
  judge: browser.judge === true,
562
801
  },
@@ -600,6 +839,7 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
600
839
  flaky: flakyConfig(asRecord(yaml.flaky)),
601
840
  release: releaseThresholds(yaml.release),
602
841
  api: apiConfig(asRecord(yaml.api)),
842
+ bi: biConfig(asRecord(yaml.bi), base, outDir, configPath),
603
843
  quality: withBaselineDir(qualityConfig(yaml.quality), base, configPath),
604
844
  domain: domainConfig(yaml.domain),
605
845
  notify: notifyConfig(yaml.notify),
@@ -836,6 +1076,38 @@ function apiConfig(a: Record<string, unknown>): ApiConfig {
836
1076
  }
837
1077
  }
838
1078
 
1079
+ function biConfig(b: Record<string, unknown>, base: string, outDir: string, configPath: string | undefined): BiConfig {
1080
+ const evidenceDir = path.resolve(base, String(b.evidence_dir || path.join(".", "evidence")))
1081
+ const defaultDataDir = path.join(evidenceDir, "sources", "forgeqe", "data")
1082
+ const title = String(b.title || "ForgeQE BI").trim() || "ForgeQE BI"
1083
+ const datasets = Object.entries(asRecord(b.datasets)).map(([id, value]) => biDataset(id, asRecord(value)))
1084
+ return {
1085
+ title,
1086
+ evidenceDir,
1087
+ dataDir: path.resolve(base, String(b.data_dir || defaultDataDir)),
1088
+ buildDir: path.resolve(base, String(b.build_dir || path.join(outDir, "bi"))),
1089
+ datasets,
1090
+ }
1091
+ }
1092
+
1093
+ function biDataset(id: string, raw: Record<string, unknown>): BiDatasetConfig {
1094
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error(`bi.datasets.${id}: ids may only use letters, digits, - and _.`)
1095
+ const title = String(raw.title || id.replace(/[-_]+/g, " ").trim()).trim()
1096
+ const kind = String(raw.kind || "").trim().toLowerCase()
1097
+ if (kind === "ado_wiql") {
1098
+ const wiql = String(raw.wiql || "").trim()
1099
+ if (!wiql) throw new Error(`bi.datasets.${id}.wiql is required for kind ado_wiql.`)
1100
+ const project = String(raw.project || "").trim()
1101
+ return { id, title: title || id, kind, wiql, ...(project ? { project } : {}) }
1102
+ }
1103
+ if (kind === "jira_jql") {
1104
+ const jql = String(raw.jql || "").trim()
1105
+ if (!jql) throw new Error(`bi.datasets.${id}.jql is required for kind jira_jql.`)
1106
+ return { id, title: title || id, kind, jql }
1107
+ }
1108
+ throw new Error(`bi.datasets.${id}.kind must be ado_wiql or jira_jql; got "${String(raw.kind || "")}".`)
1109
+ }
1110
+
839
1111
  function optional<K extends string, V>(key: K, value: V | undefined): Partial<Record<K, V>> {
840
1112
  return value === undefined ? {} : ({ [key]: value } as Record<K, V>)
841
1113
  }
@@ -3,9 +3,9 @@ import { createInterface } from "node:readline"
3
3
 
4
4
  import type { LlmProfile } from "../llm.ts"
5
5
  import type { AgentEvent, CaseReport, MemoryInfo, ReplayMode, RunResult } from "../types.ts"
6
- import { loginEnvNames } from "../childEnv.ts"
6
+ import { browserProviderEnvNames, loginEnvNames } from "../childEnv.ts"
7
7
  import { recordUsage } from "../usage.ts"
8
- import type { LoginConfig } from "../config.ts"
8
+ import type { BrowserProvider, BrowserRemoteConfig, LoginConfig } from "../config.ts"
9
9
  import { debugLogging, logEvent } from "../sessionLog.ts"
10
10
  import { engineEnv } from "./setup.ts"
11
11
  import { AZURE_IDENTITY_ENV, entraPayload } from "../entra.ts"
@@ -18,6 +18,7 @@ export type EngineRequest = {
18
18
  headed: boolean
19
19
  maxSteps: number
20
20
  allowedHosts: string[]
21
+ browser?: { provider: BrowserProvider; remote?: BrowserRemoteConfig }
21
22
  extendSystemMessage?: string
22
23
  /** Page memory folder and replay mode; omit to run without memory. */
23
24
  memory?: { dir: string; replay: ReplayMode }
@@ -148,6 +149,33 @@ function llmPayload(p: LlmProfile): Record<string, unknown> {
148
149
  return out
149
150
  }
150
151
 
152
+ function browserPayload(browser: EngineRequest["browser"]): Record<string, unknown> | null {
153
+ if (!browser) return null
154
+ return {
155
+ provider: browser.provider,
156
+ ...(browser.remote
157
+ ? {
158
+ remote: {
159
+ provider: browser.remote.provider,
160
+ username_env: browser.remote.usernameEnv,
161
+ access_key_env: browser.remote.accessKeyEnv,
162
+ endpoint: browser.remote.endpoint,
163
+ browser: browser.remote.browser,
164
+ browser_version: browser.remote.browserVersion,
165
+ ...(browser.remote.os ? { os: browser.remote.os } : {}),
166
+ ...(browser.remote.osVersion ? { os_version: browser.remote.osVersion } : {}),
167
+ ...(browser.remote.platform ? { platform: browser.remote.platform } : {}),
168
+ ...(browser.remote.project ? { project: browser.remote.project } : {}),
169
+ ...(browser.remote.build ? { build: browser.remote.build } : {}),
170
+ ...(browser.remote.name ? { name: browser.remote.name } : {}),
171
+ local: browser.remote.local,
172
+ ...(browser.remote.extra ? { extra: browser.remote.extra } : {}),
173
+ },
174
+ }
175
+ : {}),
176
+ }
177
+ }
178
+
151
179
  export function toRunResult(goal: string, startUrl: string, r: EngineResult): RunResult {
152
180
  // The agent's evidence wins over its verdict: a check that does not hold, or a failed case step, fails the run even
153
181
  // when the agent called done with success (engines before this rule did not apply it themselves).
@@ -198,6 +226,7 @@ export async function runEngine(opts: {
198
226
  const child = spawn(bin!, args, {
199
227
  env: engineEnv(debugLogging() ? { ...req.env, BROWSER_USE_LOGGING_LEVEL: "debug" } : req.env, [
200
228
  ...(req.llm.auth === "entra" ? AZURE_IDENTITY_ENV : req.llm.apiKeyEnv ? [req.llm.apiKeyEnv] : []),
229
+ ...browserProviderEnvNames(req.browser || { provider: "local" }),
201
230
  ...loginEnvNames(req.logins || []),
202
231
  ]),
203
232
  stdio: ["pipe", "pipe", "pipe"],
@@ -222,6 +251,7 @@ export async function runEngine(opts: {
222
251
  headed: req.headed,
223
252
  max_steps: req.maxSteps,
224
253
  allowed_hosts: req.allowedHosts,
254
+ browser: browserPayload(req.browser),
225
255
  extend_system_message: req.extendSystemMessage || null,
226
256
  memory: req.memory || null,
227
257
  logins: loginPayload(req.logins),