@diegosouzacdv/jev-browser-mcp 0.1.0
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/README.md +1728 -0
- package/config/ui-testing.json +23 -0
- package/docs/jev-browser-mcp.md +193 -0
- package/mcp_servers/jev-browser-npm/bin/jev-browser-mcp.cjs +34 -0
- package/mcp_servers/jev-browser-npm/src/config.mjs +162 -0
- package/mcp_servers/jev-browser-npm/src/flow.mjs +365 -0
- package/mcp_servers/jev-browser-npm/src/jev-client.mjs +129 -0
- package/mcp_servers/jev-browser-npm/src/server.mjs +206 -0
- package/package.json +34 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"browser": {
|
|
4
|
+
"mode": "harness",
|
|
5
|
+
"harness_browser": "chrome",
|
|
6
|
+
"playwright_mcp_package": "@playwright/mcp@0.0.79",
|
|
7
|
+
"computer_browser": "chrome",
|
|
8
|
+
"computer_user_data_dir": "~/.cache/orquestrador/jev-browser-{browser}",
|
|
9
|
+
"max_flow_steps": 24,
|
|
10
|
+
"max_text_entry_chars": 2000
|
|
11
|
+
},
|
|
12
|
+
"jev": {
|
|
13
|
+
"provider_url": "https://openrouter.ai/api/alpha/decisions",
|
|
14
|
+
"credential_env": "OPENROUTER_API_KEY",
|
|
15
|
+
"model": "typesafe/jev-1.13",
|
|
16
|
+
"request_timeout_seconds": 30,
|
|
17
|
+
"max_flow_chars": 12000,
|
|
18
|
+
"max_snapshot_chars": 12000,
|
|
19
|
+
"max_action_count": 32,
|
|
20
|
+
"max_action_description_chars": 300,
|
|
21
|
+
"max_response_bytes": 65536
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Automação de tela com Jev e Playwright
|
|
2
|
+
|
|
3
|
+
## Instalar em outros harnesses com npm/npx
|
|
4
|
+
|
|
5
|
+
O repositório também contém um pacote Node independente do harness. Ele fala
|
|
6
|
+
MCP por `stdio`, executa o Playwright no mesmo processo e pode ser iniciado por
|
|
7
|
+
qualquer harness que aceite `command` e `args` para um servidor MCP. O pacote
|
|
8
|
+
usa o mesmo `config/ui-testing.json` deste repositório; não precisa instalar o
|
|
9
|
+
Python do orquestrador.
|
|
10
|
+
|
|
11
|
+
Depois que a versão desejada estiver disponível no GitHub, configure o harness
|
|
12
|
+
para iniciar o pacote pela referência Git. Use uma tag ou SHA publicado para
|
|
13
|
+
fixar a versão:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"jev-browser": {
|
|
19
|
+
"command": "npx",
|
|
20
|
+
"args": ["--yes", "github:diegosouzacdv/orquestrador#<tag-ou-sha>"],
|
|
21
|
+
"env": {
|
|
22
|
+
"OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}",
|
|
23
|
+
"JEV_BROWSER_MODE": "harness"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
O formato de interpolação de variáveis varia por harness. Injete a chave por
|
|
31
|
+
um secret manager ou pelo ambiente do processo; não grave a chave no arquivo
|
|
32
|
+
de configuração. Para instalar no projeto Node do próprio harness:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install github:diegosouzacdv/orquestrador#<tag-ou-sha>
|
|
36
|
+
npx jev-browser-mcp --install-browser
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
No modo `harness`, a instalação baixa uma vez o Chrome/Edge que o Playwright
|
|
40
|
+
controlará. No modo `computer`, o pacote abre o Chrome/Edge instalado e usa um
|
|
41
|
+
perfil persistente exclusivo em `browser.computer_user_data_dir`; personalize
|
|
42
|
+
as escolhas com `JEV_BROWSER_MODE`, `JEV_BROWSER_CHANNEL` e
|
|
43
|
+
`JEV_BROWSER_PROFILE`. O browser permanece aquecido enquanto o processo MCP
|
|
44
|
+
estiver ativo e fecha quando o harness encerra o processo. `JEV_PROVIDER_URL`
|
|
45
|
+
e `JEV_MODEL` podem substituir os valores centrais; a credencial continua no
|
|
46
|
+
nome de variável declarado em `jev.credential_env`.
|
|
47
|
+
|
|
48
|
+
Uma aplicação Node também pode importar `createJevBrowserServer` por
|
|
49
|
+
`@diegosouzacdv/jev-browser-mcp/server` e conectar o servidor ao transporte MCP
|
|
50
|
+
que ela já utiliza.
|
|
51
|
+
|
|
52
|
+
O pacote é montado pela raiz do repositório, mas o campo `files` do `package.json`
|
|
53
|
+
inclui somente o código Node, a configuração compartilhada e esta documentação
|
|
54
|
+
(além do README que o npm inclui automaticamente). Ele não publica o restante
|
|
55
|
+
do orquestrador. A instalação por Git passa a ser possível quando a referência
|
|
56
|
+
escolhida tiver sido enviada ao GitHub.
|
|
57
|
+
|
|
58
|
+
### Contrato do pacote
|
|
59
|
+
|
|
60
|
+
O executável oferece `choose_next_action` e `run_browser_flow`, mantém uma
|
|
61
|
+
sessão do browser por processo e reutiliza essa sessão entre chamadas. O plano
|
|
62
|
+
passado ao Jev continua declarativo e limitado a clique por papel/nome
|
|
63
|
+
acessível, preenchimento de texto, espera por texto, teclas de rolagem e
|
|
64
|
+
reações idempotentes a um comentário único. Não aceita JavaScript, seletores
|
|
65
|
+
livres nem coordenadas. Ele usa a biblioteca Playwright diretamente, sem iniciar
|
|
66
|
+
um segundo servidor MCP do Playwright. O transporte MCP usa `stdio`; toda saída
|
|
67
|
+
de diagnóstico vai para `stderr` para não misturar com JSON-RPC.
|
|
68
|
+
|
|
69
|
+
O modo `computer` grava cookies no perfil exclusivo configurado, e conteúdo da
|
|
70
|
+
página pode conter instruções maliciosas. O Jev recebe a captura acessível com
|
|
71
|
+
uma instrução para tratar esse conteúdo como dado não confiável; não inclua
|
|
72
|
+
segredos no fluxo, no resultado esperado ou nas descrições dos planos.
|
|
73
|
+
|
|
74
|
+
Para um fluxo conhecido, o LLM do harness procura o cenário e os critérios de
|
|
75
|
+
aceitação no projeto e chama `jev_browser.run_browser_flow` com planos
|
|
76
|
+
candidatos declarativos. O MCP abre uma única sessão do Playwright, navega para
|
|
77
|
+
a página inicial, captura o snapshot acessível e pede ao Jev que escolha um
|
|
78
|
+
plano. Em seguida, executa o plano inteiro na mesma sessão e confere o
|
|
79
|
+
resultado esperado na tela.
|
|
80
|
+
|
|
81
|
+
Cada plano pode usar `click`, `type`, `wait_for_text`, teclas aprovadas de
|
|
82
|
+
navegação/rolagem (`PageDown`, `PageUp`, `Home`, `End`, `ArrowDown`, `ArrowUp`)
|
|
83
|
+
e as ações `like_comment` e `unlike_comment`. Cliques e entrada de texto usam
|
|
84
|
+
papel e nome acessível exatos; as ações de reação localizam uma única linha pelo
|
|
85
|
+
autor e texto, inspecionam os controles dentro dela e não clicam novamente
|
|
86
|
+
quando já estão no estado pedido. O rótulo é comparado como palavra inteira
|
|
87
|
+
para que `Curtir` não seja confundido com `Descurtir`. Não há JavaScript
|
|
88
|
+
arbitrário, coordenadas ou seletores livres. Se um alvo estiver
|
|
89
|
+
ausente ou ambíguo, o MCP interrompe o plano e devolve a última evidência para o
|
|
90
|
+
harness decidir como continuar.
|
|
91
|
+
|
|
92
|
+
Exemplo de chamada:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"flow": "Adicionar o produto ao carrinho e confirmar o resumo",
|
|
97
|
+
"initial_url": "http://127.0.0.1:4173/products/coffee",
|
|
98
|
+
"expected_outcome": "Coffee added to cart",
|
|
99
|
+
"candidate_plans": {
|
|
100
|
+
"add_and_confirm": {
|
|
101
|
+
"description": "Adicionar o produto visível ao carrinho e abrir o resumo",
|
|
102
|
+
"steps": [
|
|
103
|
+
{"action": "click", "role": "button", "name": "Add to cart"},
|
|
104
|
+
{"action": "wait_for_text", "text": "Coffee added to cart"},
|
|
105
|
+
{"action": "click", "role": "link", "name": "View cart"}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
O plano é montado pelo LLM do harness, mas o Jev escolhe qual plano fornecido
|
|
113
|
+
deve executar usando o fluxo e o snapshot inicial. O Jev não cria ações, nomes
|
|
114
|
+
de controles ou valores de formulário. Valores de `type` são usados localmente
|
|
115
|
+
pelo Playwright e são removidos do texto enviado ao provedor e da evidência de
|
|
116
|
+
retorno. Use valores de teste; autenticação deve ficar no perfil de navegador
|
|
117
|
+
configurado.
|
|
118
|
+
|
|
119
|
+
O MCP também mantém `choose_next_action` para fluxos exploratórios em que o
|
|
120
|
+
harness precisa inspecionar e decidir entre ações uma por vez. Esse caminho é
|
|
121
|
+
mais lento porque exige uma nova decisão e uma nova chamada de ferramenta por
|
|
122
|
+
ação; prefira `run_browser_flow` quando os passos esperados puderem ser
|
|
123
|
+
descritos antes da execução.
|
|
124
|
+
|
|
125
|
+
## Configuração
|
|
126
|
+
|
|
127
|
+
Edite `config/ui-testing.json`. O próprio arquivo declara URL e modelo do
|
|
128
|
+
provedor, nome da variável de credencial e limites de entrada e resposta;
|
|
129
|
+
preserve a estrutura completa exigida pelo validador.
|
|
130
|
+
|
|
131
|
+
`browser.mode` aceita `harness` ou `computer`:
|
|
132
|
+
|
|
133
|
+
- `harness` usa Chrome headless e perfil isolado, adequado a execuções do
|
|
134
|
+
harness e CI; o estado de autenticação é descartado ao final da chamada.
|
|
135
|
+
- `computer` abre o Chrome ou Edge instalado em modo visível e usa o diretório
|
|
136
|
+
persistente `browser.computer_user_data_dir`, separado por navegador. Não
|
|
137
|
+
reutiliza o perfil pessoal já aberto. Faça login uma vez nesse perfil; os
|
|
138
|
+
cookies permanecem nele entre chamadas.
|
|
139
|
+
|
|
140
|
+
`browser.max_flow_steps` limita a soma de passos declarados entre os planos e
|
|
141
|
+
`browser.max_text_entry_chars` limita cada valor digitado. O resultado contém
|
|
142
|
+
`status`, o plano escolhido, as ações executadas, a última captura acessível e
|
|
143
|
+
se o critério esperado apareceu. `incomplete` significa que o fluxo não foi
|
|
144
|
+
comprovado; confiança do Jev não substitui essa verificação.
|
|
145
|
+
|
|
146
|
+
`harness_browser` e `computer_browser` aceitam `chrome` ou `msedge`.
|
|
147
|
+
`computer_user_data_dir` deve ficar fora do repositório e conter `{browser}`;
|
|
148
|
+
o diretório persistente armazena dados de login e é resolvido sob a pasta home
|
|
149
|
+
do usuário quando começa com `~`. O Playwright MCP fica desabilitado até
|
|
150
|
+
`ORQUESTRADOR_MCP_PLAYWRIGHT_ENABLED=1` ser configurado no ambiente do harness.
|
|
151
|
+
|
|
152
|
+
Defina o valor secreto na variável indicada por `jev.credential_env`, usando o
|
|
153
|
+
secret manager ou ambiente do processo que inicia o harness. Não grave a chave
|
|
154
|
+
em `config/ui-testing.json`. `provider_url` é o endpoint HTTPS completo da API
|
|
155
|
+
Decisions. O cliente não
|
|
156
|
+
segue redirects e recusa URL com credencial, query string ou fragmento. O Jev
|
|
157
|
+
fica indisponível quando a política de MCP está em modo offline.
|
|
158
|
+
|
|
159
|
+
Na primeira execução, aqueça uma vez o cache local do pacote declarado em
|
|
160
|
+
`browser.playwright_mcp_package` com `npx --yes <pacote> --help`. O MCP inicia
|
|
161
|
+
depois com `--offline`, evitando uma consulta ao registry npm em cada fluxo.
|
|
162
|
+
Quando a versão configurada mudar, aqueça o novo pacote uma vez.
|
|
163
|
+
|
|
164
|
+
## Desempenho e evidência
|
|
165
|
+
|
|
166
|
+
O servidor Python `mcp_servers/jev_browser_server.py` usa uma decisão remota do
|
|
167
|
+
Jev por fluxo. Inclua o fluxo
|
|
168
|
+
completo em um plano candidato para evitar chamadas separadas ao harness; a
|
|
169
|
+
seleção do plano, a navegação e a reação ao comentário ficam em uma chamada MCP.
|
|
170
|
+
A sessão Playwright fecha antes de o MCP Python retornar. O perfil do modo
|
|
171
|
+
`computer` preserva o login para chamadas seguintes; nesse servidor o processo
|
|
172
|
+
e a janela não são reutilizados. O pacote Node mantém o browser aquecido até o
|
|
173
|
+
harness encerrar o processo. O snapshot enviado ao Jev e
|
|
174
|
+
devolvido ao harness remove o rodapé e, se ainda exceder o limite, mantém o
|
|
175
|
+
início e o fim da captura com um marcador de truncamento.
|
|
176
|
+
|
|
177
|
+
A chamada composta aquecida deve ficar dentro da meta de 20 segundos em páginas
|
|
178
|
+
que respondem normalmente; a inicialização fria, páginas lentas, MFA e conteúdo
|
|
179
|
+
sob demanda podem excedê-la. O MCP devolve `total_ms`, `browser_session_ms`,
|
|
180
|
+
`navigation_ms`, `initial_snapshot_ms`, `jev_decision_ms` e `browser_plan_ms`
|
|
181
|
+
para localizar o custo. O teto observado em um fluxo sintético local anterior
|
|
182
|
+
foi 6,411 s; isso não mede o Instagram nem garante o mesmo tempo em outros sites.
|
|
183
|
+
|
|
184
|
+
O snapshot inicial, o fluxo, o resultado esperado e as descrições dos planos
|
|
185
|
+
são enviados ao endpoint Decisions. Os passos e valores de texto dos planos
|
|
186
|
+
não são enviados. Não inclua segredos em `flow`, `expected_outcome` ou nas
|
|
187
|
+
descrições. A resposta retorna o plano escolhido, custo/confiança do provedor
|
|
188
|
+
quando disponíveis e o snapshot final sanitizado. O fluxo só passa quando o
|
|
189
|
+
texto esperado aparece nesse snapshot.
|
|
190
|
+
|
|
191
|
+
Consulte a [introdução do Jev](https://docs.typesafe.ai/introduction) e o
|
|
192
|
+
[tutorial da API Decisions no OpenRouter](https://openrouter.ai/docs/guides/community/jev-tutorial)
|
|
193
|
+
para os tipos de resposta e autenticação.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const usage = `jev-browser-mcp - Jev + Playwright MCP server
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
jev-browser-mcp Start the MCP stdio server
|
|
8
|
+
jev-browser-mcp --install-browser
|
|
9
|
+
Install the configured browser for harness mode
|
|
10
|
+
jev-browser-mcp --help Show this help
|
|
11
|
+
|
|
12
|
+
Configuration is read from config/ui-testing.json. Set OPENROUTER_API_KEY in
|
|
13
|
+
the environment and use JEV_BROWSER_MODE=harness or computer to choose a browser.
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
19
|
+
process.stdout.write(usage);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const server = await import("../src/server.mjs");
|
|
23
|
+
if (args.length === 1 && args[0] === "--install-browser") {
|
|
24
|
+
process.exitCode = await server.installConfiguredBrowser();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (args.length > 0) throw new Error("Unknown argument. Run jev-browser-mcp --help.");
|
|
28
|
+
server.runStdioServer();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
main().catch((error) => {
|
|
32
|
+
process.stderr.write(`${error?.message || "Could not start jev-browser-mcp"}\n`);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
7
|
+
const CONFIG_PATH = path.join(PACKAGE_ROOT, "config", "ui-testing.json");
|
|
8
|
+
const CONFIG_KEYS = {
|
|
9
|
+
root: ["version", "browser", "jev"],
|
|
10
|
+
browser: [
|
|
11
|
+
"mode",
|
|
12
|
+
"harness_browser",
|
|
13
|
+
"computer_browser",
|
|
14
|
+
"playwright_mcp_package",
|
|
15
|
+
"computer_user_data_dir",
|
|
16
|
+
"max_flow_steps",
|
|
17
|
+
"max_text_entry_chars",
|
|
18
|
+
],
|
|
19
|
+
jev: [
|
|
20
|
+
"provider_url",
|
|
21
|
+
"credential_env",
|
|
22
|
+
"model",
|
|
23
|
+
"request_timeout_seconds",
|
|
24
|
+
"max_flow_chars",
|
|
25
|
+
"max_snapshot_chars",
|
|
26
|
+
"max_action_count",
|
|
27
|
+
"max_action_description_chars",
|
|
28
|
+
"max_response_bytes",
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export class JevBrowserError extends Error {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "JevBrowserError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function record(value) {
|
|
40
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requireKeys(value, keys, scope) {
|
|
44
|
+
if (!record(value)) throw new JevBrowserError(`config/ui-testing.json ${scope} must be an object`);
|
|
45
|
+
const actual = Object.keys(value).sort();
|
|
46
|
+
const expected = [...keys].sort();
|
|
47
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
48
|
+
throw new JevBrowserError(`config/ui-testing.json ${scope} has missing or unsupported fields`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function positiveNumber(value, name, integer = false) {
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || (integer && !Number.isInteger(parsed))) {
|
|
55
|
+
throw new JevBrowserError(`config/ui-testing.json ${name} must be a positive ${integer ? "integer" : "number"}`);
|
|
56
|
+
}
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validProviderUrl(value) {
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = new URL(value);
|
|
64
|
+
} catch {
|
|
65
|
+
throw new JevBrowserError("Jev provider URL must be a valid HTTPS endpoint");
|
|
66
|
+
}
|
|
67
|
+
if (parsed.protocol !== "https:" || !parsed.hostname || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
68
|
+
throw new JevBrowserError("Jev provider URL must be HTTPS without credentials, query, or fragment");
|
|
69
|
+
}
|
|
70
|
+
return parsed.toString();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function profileDirectory(template, channel, override) {
|
|
74
|
+
let value = override?.trim() || template.replace("{browser}", channel);
|
|
75
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
76
|
+
value = path.join(homedir(), value.slice(2));
|
|
77
|
+
}
|
|
78
|
+
if (!path.isAbsolute(value)) throw new JevBrowserError("JEV_BROWSER_PROFILE must be an absolute path");
|
|
79
|
+
const resolved = path.resolve(value);
|
|
80
|
+
const insidePackage = path.relative(PACKAGE_ROOT, resolved);
|
|
81
|
+
if (insidePackage === "" || (!insidePackage.startsWith(`..${path.sep}`) && insidePackage !== ".." && !path.isAbsolute(insidePackage))) {
|
|
82
|
+
throw new JevBrowserError("browser profile must be outside the installed package directory");
|
|
83
|
+
}
|
|
84
|
+
return resolved;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function loadSettings({ env = process.env, configPath = CONFIG_PATH } = {}) {
|
|
88
|
+
let document;
|
|
89
|
+
try {
|
|
90
|
+
document = JSON.parse(readFileSync(configPath, "utf8"));
|
|
91
|
+
} catch {
|
|
92
|
+
throw new JevBrowserError("config/ui-testing.json is unreadable or invalid JSON");
|
|
93
|
+
}
|
|
94
|
+
requireKeys(document, CONFIG_KEYS.root, "root");
|
|
95
|
+
requireKeys(document.browser, CONFIG_KEYS.browser, "browser");
|
|
96
|
+
requireKeys(document.jev, CONFIG_KEYS.jev, "jev");
|
|
97
|
+
if (document.version !== 1) throw new JevBrowserError("config/ui-testing.json has an unsupported version");
|
|
98
|
+
|
|
99
|
+
if (!new Set(["harness", "computer"]).has(document.browser.mode)) {
|
|
100
|
+
throw new JevBrowserError("config/ui-testing.json browser.mode must be harness or computer");
|
|
101
|
+
}
|
|
102
|
+
if (!new Set(["chrome", "msedge"]).has(document.browser.harness_browser)
|
|
103
|
+
|| !new Set(["chrome", "msedge"]).has(document.browser.computer_browser)) {
|
|
104
|
+
throw new JevBrowserError("config/ui-testing.json browser channels must be chrome or msedge");
|
|
105
|
+
}
|
|
106
|
+
if (typeof document.browser.playwright_mcp_package !== "string"
|
|
107
|
+
|| !/^@playwright\/mcp@\d+\.\d+\.\d+$/.test(document.browser.playwright_mcp_package)) {
|
|
108
|
+
throw new JevBrowserError("config/ui-testing.json browser.playwright_mcp_package must pin an exact MCP version");
|
|
109
|
+
}
|
|
110
|
+
const mode = (env.JEV_BROWSER_MODE || document.browser.mode).trim();
|
|
111
|
+
if (!new Set(["harness", "computer"]).has(mode)) {
|
|
112
|
+
throw new JevBrowserError("JEV_BROWSER_MODE must be harness or computer");
|
|
113
|
+
}
|
|
114
|
+
const configuredChannel = mode === "harness" ? document.browser.harness_browser : document.browser.computer_browser;
|
|
115
|
+
const channel = (env.JEV_BROWSER_CHANNEL || configuredChannel).trim();
|
|
116
|
+
if (!new Set(["chrome", "msedge"]).has(channel)) {
|
|
117
|
+
throw new JevBrowserError("JEV_BROWSER_CHANNEL must be chrome or msedge");
|
|
118
|
+
}
|
|
119
|
+
const profileTemplate = document.browser.computer_user_data_dir;
|
|
120
|
+
const profileRemainder = typeof profileTemplate === "string" ? profileTemplate.replace("{browser}", "") : "";
|
|
121
|
+
if (typeof profileTemplate !== "string" || profileTemplate.split("{browser}").length !== 2
|
|
122
|
+
|| profileRemainder.includes("{") || profileRemainder.includes("}")) {
|
|
123
|
+
throw new JevBrowserError("config/ui-testing.json browser.computer_user_data_dir must contain {browser}");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const configuredProviderUrl = validProviderUrl(document.jev.provider_url);
|
|
127
|
+
const providerUrl = validProviderUrl(env.JEV_PROVIDER_URL?.trim() || configuredProviderUrl);
|
|
128
|
+
const credentialEnv = String(document.jev.credential_env || "").trim();
|
|
129
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(credentialEnv)) {
|
|
130
|
+
throw new JevBrowserError("config/ui-testing.json jev.credential_env must be an environment variable name");
|
|
131
|
+
}
|
|
132
|
+
const configuredModel = String(document.jev.model || "").trim();
|
|
133
|
+
if (!configuredModel) throw new JevBrowserError("config/ui-testing.json jev.model is required");
|
|
134
|
+
const model = (env.JEV_MODEL?.trim() || configuredModel).trim();
|
|
135
|
+
if (!model) throw new JevBrowserError("Jev model must not be empty");
|
|
136
|
+
|
|
137
|
+
return Object.freeze({
|
|
138
|
+
env,
|
|
139
|
+
browser: Object.freeze({
|
|
140
|
+
mode,
|
|
141
|
+
channel,
|
|
142
|
+
profileDir: profileDirectory(profileTemplate, channel, env.JEV_BROWSER_PROFILE),
|
|
143
|
+
maxFlowSteps: positiveNumber(document.browser.max_flow_steps, "browser.max_flow_steps", true),
|
|
144
|
+
maxTextEntryChars: positiveNumber(document.browser.max_text_entry_chars, "browser.max_text_entry_chars", true),
|
|
145
|
+
}),
|
|
146
|
+
jev: Object.freeze({
|
|
147
|
+
providerUrl,
|
|
148
|
+
credentialEnv,
|
|
149
|
+
model,
|
|
150
|
+
requestTimeoutMs: positiveNumber(document.jev.request_timeout_seconds, "jev.request_timeout_seconds") * 1000,
|
|
151
|
+
maxFlowChars: positiveNumber(document.jev.max_flow_chars, "jev.max_flow_chars", true),
|
|
152
|
+
maxSnapshotChars: positiveNumber(document.jev.max_snapshot_chars, "jev.max_snapshot_chars", true),
|
|
153
|
+
maxActionCount: positiveNumber(document.jev.max_action_count, "jev.max_action_count", true),
|
|
154
|
+
maxActionDescriptionChars: positiveNumber(document.jev.max_action_description_chars, "jev.max_action_description_chars", true),
|
|
155
|
+
maxResponseBytes: positiveNumber(document.jev.max_response_bytes, "jev.max_response_bytes", true),
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function packageRoot() {
|
|
161
|
+
return PACKAGE_ROOT;
|
|
162
|
+
}
|