@debugg-ai/debugg-ai-mcp 2.8.0 → 2.8.1
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/dist/config/index.js +11 -1
- package/dist/handlers/probePageHandler.js +48 -41
- package/dist/handlers/runTestSuiteHandler.js +40 -34
- package/dist/handlers/testPageChangesHandler.js +63 -57
- package/dist/handlers/triggerCrawlHandler.js +43 -37
- package/dist/services/workflows.js +17 -1
- package/package.json +1 -1
package/dist/config/index.js
CHANGED
|
@@ -40,6 +40,10 @@ function isTelemetryDisabled() {
|
|
|
40
40
|
const v = (process.env.DEBUGGAI_TELEMETRY_DISABLED || '').toLowerCase();
|
|
41
41
|
return v === '1' || v === 'true' || v === 'yes' || v === 'on';
|
|
42
42
|
}
|
|
43
|
+
function isDevMode() {
|
|
44
|
+
const v = (process.env.DEBUGGAI_DEV_MODE || '').toLowerCase();
|
|
45
|
+
return v === '1' || v === 'true' || v === 'yes' || v === 'on';
|
|
46
|
+
}
|
|
43
47
|
function resolvePosthogKey() {
|
|
44
48
|
if (isTelemetryDisabled())
|
|
45
49
|
return undefined;
|
|
@@ -50,6 +54,7 @@ const configSchema = z.object({
|
|
|
50
54
|
name: z.string().default('DebuggAI MCP Server'),
|
|
51
55
|
version: z.string(),
|
|
52
56
|
}),
|
|
57
|
+
devMode: z.boolean().default(false),
|
|
53
58
|
api: z.object({
|
|
54
59
|
// key is validated at tool-call time (not at boot) so MCP clients can surface
|
|
55
60
|
// a proper error message instead of seeing the subprocess die → "Failed to
|
|
@@ -74,11 +79,12 @@ export function loadConfig() {
|
|
|
74
79
|
name: 'DebuggAI MCP Server',
|
|
75
80
|
version: _version,
|
|
76
81
|
},
|
|
82
|
+
devMode: isDevMode(),
|
|
77
83
|
api: {
|
|
78
84
|
// Priority: DEBUGGAI_API_TOKEN → DEBUGGAI_JWT_TOKEN → DEBUGGAI_API_KEY
|
|
79
85
|
key: process.env.DEBUGGAI_API_TOKEN || process.env.DEBUGGAI_JWT_TOKEN || process.env.DEBUGGAI_API_KEY || '',
|
|
80
86
|
tokenType: process.env.DEBUGGAI_TOKEN_TYPE || 'token',
|
|
81
|
-
baseUrl: process.env.DEBUGGAI_API_URL || 'https://api.debugg.ai',
|
|
87
|
+
baseUrl: process.env.DEBUGGAI_API_URL || (isDevMode() ? 'http://localhost:8012' : 'https://api.debugg.ai'),
|
|
82
88
|
},
|
|
83
89
|
defaults: {},
|
|
84
90
|
logging: {
|
|
@@ -106,6 +112,7 @@ export function loadConfig() {
|
|
|
106
112
|
let _config;
|
|
107
113
|
export const config = {
|
|
108
114
|
get server() { return getConfig().server; },
|
|
115
|
+
get devMode() { return getConfig().devMode; },
|
|
109
116
|
get api() { return getConfig().api; },
|
|
110
117
|
get defaults() { return getConfig().defaults; },
|
|
111
118
|
get logging() { return getConfig().logging; },
|
|
@@ -117,3 +124,6 @@ function getConfig() {
|
|
|
117
124
|
}
|
|
118
125
|
return _config;
|
|
119
126
|
}
|
|
127
|
+
export function _resetConfigForTest() {
|
|
128
|
+
_config = undefined;
|
|
129
|
+
}
|
|
@@ -89,53 +89,60 @@ export async function probePageHandler(input, context, rawProgressCallback) {
|
|
|
89
89
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
targetContexts.push(
|
|
92
|
+
if (config.devMode) {
|
|
93
|
+
// Dev mode: local backend can reach localhost directly — no tunnel needed.
|
|
94
|
+
logger.info(`probe_page: dev mode — using localhost URL directly: ${ctx.originalUrl}`);
|
|
95
|
+
targetContexts.push(ctx);
|
|
96
96
|
}
|
|
97
97
|
else {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
// Reuse existing tunnel for this port if any; otherwise provision.
|
|
99
|
+
const reused = findExistingTunnel(ctx);
|
|
100
|
+
if (reused) {
|
|
101
|
+
targetContexts.push(reused);
|
|
101
102
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
103
|
+
else {
|
|
104
|
+
let tunnel;
|
|
105
|
+
try {
|
|
106
|
+
tunnel = await client.tunnels.provisionWithRetry();
|
|
107
|
+
}
|
|
108
|
+
catch (provisionError) {
|
|
109
|
+
const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
|
|
110
|
+
const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
|
|
111
|
+
throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}. ` +
|
|
112
|
+
`(Detail: ${msg})${diag}`);
|
|
113
|
+
}
|
|
114
|
+
acquiredKeyIds.push(tunnel.keyId);
|
|
115
|
+
let tunneled;
|
|
116
|
+
try {
|
|
117
|
+
tunneled = await ensureTunnel(ctx, tunnel.tunnelKey, tunnel.tunnelId, tunnel.keyId, () => client.revokeNgrokKey(tunnel.keyId));
|
|
118
|
+
}
|
|
119
|
+
catch (tunnelError) {
|
|
120
|
+
const msg = tunnelError instanceof Error ? tunnelError.message : String(tunnelError);
|
|
121
|
+
throw new Error(`Tunnel creation failed for ${ctx.originalUrl}. (Detail: ${msg})`);
|
|
122
|
+
}
|
|
123
|
+
// Tunnel health probe: catch the IPv4/IPv6 bind / dead-server case
|
|
124
|
+
// before committing to a full backend execution.
|
|
125
|
+
if (tunneled.targetUrl) {
|
|
126
|
+
const health = await probeTunnelHealth(tunneled.targetUrl);
|
|
127
|
+
if (!health.healthy) {
|
|
128
|
+
const payload = {
|
|
129
|
+
error: 'TunnelTrafficBlocked',
|
|
130
|
+
message: `Tunnel established but traffic isn't reaching the dev server. ${health.detail ?? ''}`,
|
|
131
|
+
detail: {
|
|
132
|
+
code: health.code,
|
|
133
|
+
status: health.status,
|
|
134
|
+
ngrokErrorCode: health.ngrokErrorCode,
|
|
135
|
+
elapsedMs: health.elapsedMs,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
if (tunneled.tunnelId) {
|
|
139
|
+
tunnelManager.stopTunnel(tunneled.tunnelId).catch((err) => logger.warn(`Failed to stop broken tunnel ${tunneled.tunnelId}: ${err}`));
|
|
140
|
+
}
|
|
141
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
134
142
|
}
|
|
135
|
-
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
136
143
|
}
|
|
144
|
+
targetContexts.push(tunneled);
|
|
137
145
|
}
|
|
138
|
-
targetContexts.push(tunneled);
|
|
139
146
|
}
|
|
140
147
|
}
|
|
141
148
|
else {
|
|
@@ -46,47 +46,53 @@ export async function runTestSuiteHandler(input, _context) {
|
|
|
46
46
|
return errorResp('LocalServerUnreachable', `No server listening on 127.0.0.1:${port}. Start your dev server before running the suite. (${probe.code}: ${probe.detail ?? 'no detail'})`, { port, probeCode: probe.code, elapsedMs: probe.elapsedMs });
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
effectiveTargetUrl = reused.targetUrl ?? input.targetUrl;
|
|
53
|
-
tunnelId = reused.tunnelId;
|
|
49
|
+
if (config.devMode) {
|
|
50
|
+
// Dev mode: local backend can reach localhost directly — no tunnel needed.
|
|
51
|
+
logger.info(`run_test_suite: dev mode — using localhost URL directly: ${input.targetUrl}`);
|
|
54
52
|
}
|
|
55
53
|
else {
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
// Reuse an existing tunnel for this port if one is already active.
|
|
55
|
+
const reused = findExistingTunnel(ctx);
|
|
56
|
+
if (reused) {
|
|
57
|
+
effectiveTargetUrl = reused.targetUrl ?? input.targetUrl;
|
|
58
|
+
tunnelId = reused.tunnelId;
|
|
60
59
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
60
|
+
else {
|
|
61
|
+
// Provision a new tunnel.
|
|
62
|
+
let tunnel;
|
|
63
|
+
try {
|
|
64
|
+
tunnel = await client.tunnels.provisionWithRetry();
|
|
65
|
+
}
|
|
66
|
+
catch (provisionError) {
|
|
67
|
+
const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
|
|
68
|
+
const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
|
|
69
|
+
return errorResp('TunnelProvisionFailed', `Failed to provision tunnel for ${input.targetUrl}. (Detail: ${msg})${diag}`);
|
|
70
|
+
}
|
|
71
|
+
acquiredKeyId = tunnel.keyId;
|
|
72
|
+
let tunneled;
|
|
73
|
+
try {
|
|
74
|
+
tunneled = await ensureTunnel(ctx, tunnel.tunnelKey, tunnel.tunnelId, tunnel.keyId, () => client.revokeNgrokKey(tunnel.keyId));
|
|
75
|
+
}
|
|
76
|
+
catch (tunnelError) {
|
|
77
|
+
const msg = tunnelError instanceof Error ? tunnelError.message : String(tunnelError);
|
|
78
|
+
return errorResp('TunnelCreationFailed', `Tunnel creation failed for ${input.targetUrl}. (Detail: ${msg})`);
|
|
79
|
+
}
|
|
80
|
+
// Health probe — catches ERR_NGROK_8012 and bind mismatches before
|
|
81
|
+
// the remote agent wastes steps trying to reach the server.
|
|
82
|
+
if (tunneled.targetUrl) {
|
|
83
|
+
const health = await probeTunnelHealth(tunneled.targetUrl);
|
|
84
|
+
if (!health.healthy) {
|
|
85
|
+
if (tunneled.tunnelId) {
|
|
86
|
+
tunnelManager.stopTunnel(tunneled.tunnelId).catch((err) => logger.warn(`Failed to stop broken tunnel ${tunneled.tunnelId}: ${err}`));
|
|
87
|
+
}
|
|
88
|
+
return errorResp('TunnelTrafficBlocked', `Tunnel established but traffic isn't reaching the dev server. ${health.detail ?? ''}`, { code: health.code, ngrokErrorCode: health.ngrokErrorCode, elapsedMs: health.elapsedMs });
|
|
82
89
|
}
|
|
83
|
-
return errorResp('TunnelTrafficBlocked', `Tunnel established but traffic isn't reaching the dev server. ${health.detail ?? ''}`, { code: health.code, ngrokErrorCode: health.ngrokErrorCode, elapsedMs: health.elapsedMs });
|
|
84
90
|
}
|
|
91
|
+
effectiveTargetUrl = tunneled.targetUrl ?? input.targetUrl;
|
|
92
|
+
tunnelId = tunneled.tunnelId;
|
|
85
93
|
}
|
|
86
|
-
|
|
87
|
-
tunnelId = tunneled.tunnelId;
|
|
94
|
+
logger.info(`run_test_suite: localhost detected, tunneled ${input.targetUrl} → ${effectiveTargetUrl}`);
|
|
88
95
|
}
|
|
89
|
-
logger.info(`run_test_suite: localhost detected, tunneled ${input.targetUrl} → ${effectiveTargetUrl}`);
|
|
90
96
|
}
|
|
91
97
|
}
|
|
92
98
|
const result = await client.runTestSuite(suiteUuid, { targetUrl: effectiveTargetUrl });
|
|
@@ -125,69 +125,75 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
|
|
|
125
125
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const reused = findExistingTunnel(ctx);
|
|
132
|
-
if (reused) {
|
|
133
|
-
ctx = reused;
|
|
134
|
-
logger.info(`Reusing tunnel: ${ctx.targetUrl} (id: ${ctx.tunnelId})`);
|
|
128
|
+
if (config.devMode) {
|
|
129
|
+
// Dev mode: local backend can reach localhost directly — no tunnel needed.
|
|
130
|
+
logger.info(`check_app_in_browser: dev mode — using localhost URL directly: ${ctx.originalUrl}`);
|
|
135
131
|
}
|
|
136
132
|
else {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
tunnel = await client.tunnels.provisionWithRetry();
|
|
140
|
-
}
|
|
141
|
-
catch (provisionError) {
|
|
142
|
-
const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
|
|
143
|
-
const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
|
|
144
|
-
throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}. ` +
|
|
145
|
-
`The remote browser needs a secure tunnel to reach your local dev server. ` +
|
|
146
|
-
`Make sure your dev server is running on the specified port and try again. ` +
|
|
147
|
-
`(Detail: ${msg})${diag}`);
|
|
133
|
+
if (progressCallback) {
|
|
134
|
+
await progressCallback({ progress: 1, total: TOTAL_STEPS, message: 'Provisioning secure tunnel for localhost...' });
|
|
148
135
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
ctx =
|
|
136
|
+
const reused = findExistingTunnel(ctx);
|
|
137
|
+
if (reused) {
|
|
138
|
+
ctx = reused;
|
|
139
|
+
logger.info(`Reusing tunnel: ${ctx.targetUrl} (id: ${ctx.tunnelId})`);
|
|
152
140
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
141
|
+
else {
|
|
142
|
+
let tunnel;
|
|
143
|
+
try {
|
|
144
|
+
tunnel = await client.tunnels.provisionWithRetry();
|
|
145
|
+
}
|
|
146
|
+
catch (provisionError) {
|
|
147
|
+
const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
|
|
148
|
+
const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
|
|
149
|
+
throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}. ` +
|
|
150
|
+
`The remote browser needs a secure tunnel to reach your local dev server. ` +
|
|
151
|
+
`Make sure your dev server is running on the specified port and try again. ` +
|
|
152
|
+
`(Detail: ${msg})${diag}`);
|
|
153
|
+
}
|
|
154
|
+
keyId = tunnel.keyId;
|
|
155
|
+
try {
|
|
156
|
+
ctx = await ensureTunnel(ctx, tunnel.tunnelKey, tunnel.tunnelId, tunnel.keyId, () => client.revokeNgrokKey(tunnel.keyId));
|
|
157
|
+
}
|
|
158
|
+
catch (tunnelError) {
|
|
159
|
+
const msg = tunnelError instanceof Error ? tunnelError.message : String(tunnelError);
|
|
160
|
+
throw new Error(`Tunnel creation failed for ${ctx.originalUrl}. ` +
|
|
161
|
+
`Could not establish a secure connection between the remote browser and your local port. ` +
|
|
162
|
+
`Verify your dev server is running and the port is accessible. ` +
|
|
163
|
+
`(Detail: ${msg})`);
|
|
164
|
+
}
|
|
165
|
+
logger.info(`Tunnel ready: ${ctx.targetUrl} (id: ${ctx.tunnelId})`);
|
|
159
166
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
167
|
+
// Bead 1om: verify traffic actually flows through the tunnel. The
|
|
168
|
+
// tunnel can be established (ngrok.connect returns OK) yet refuse
|
|
169
|
+
// to forward traffic — e.g., IPv4/IPv6 bind mismatch, or the dev
|
|
170
|
+
// server died between the pre-flight probe and here. Catch it now,
|
|
171
|
+
// in ~1s, not via a 5-minute browser-agent false-pass.
|
|
172
|
+
if (ctx.targetUrl) {
|
|
173
|
+
const health = await probeTunnelHealth(ctx.targetUrl);
|
|
174
|
+
if (!health.healthy) {
|
|
175
|
+
const payload = {
|
|
176
|
+
error: 'TunnelTrafficBlocked',
|
|
177
|
+
message: `Tunnel was established but traffic isn't reaching the dev server. ${health.detail ?? ''} Common causes: dev server binds to 0.0.0.0 or ::1 but not 127.0.0.1; dev server crashed; firewall.`,
|
|
178
|
+
detail: {
|
|
179
|
+
code: health.code,
|
|
180
|
+
status: health.status,
|
|
181
|
+
ngrokErrorCode: health.ngrokErrorCode,
|
|
182
|
+
elapsedMs: health.elapsedMs,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
logger.warn(`Tunnel health probe failed for ${ctx.targetUrl}: ${health.code} ${health.ngrokErrorCode ?? ''} in ${health.elapsedMs}ms`);
|
|
186
|
+
// Tear down the broken tunnel so a subsequent call doesn't reuse it.
|
|
187
|
+
// stopTunnel handles both owned (ngrok disconnect + key revoke) and
|
|
188
|
+
// borrowed (just drops local ref) cases.
|
|
189
|
+
if (ctx.tunnelId) {
|
|
190
|
+
tunnelManager.stopTunnel(ctx.tunnelId).catch((err) => logger.warn(`Failed to stop broken tunnel ${ctx.tunnelId}: ${err}`));
|
|
191
|
+
}
|
|
192
|
+
// keyId is consumed by stopTunnel's revoke path; clear so the
|
|
193
|
+
// outer finally block doesn't double-revoke.
|
|
194
|
+
keyId = undefined;
|
|
195
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
186
196
|
}
|
|
187
|
-
// keyId is consumed by stopTunnel's revoke path; clear so the
|
|
188
|
-
// outer finally block doesn't double-revoke.
|
|
189
|
-
keyId = undefined;
|
|
190
|
-
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
191
197
|
}
|
|
192
198
|
}
|
|
193
199
|
}
|
|
@@ -82,48 +82,54 @@ export async function triggerCrawlHandler(input, context, rawProgressCallback) {
|
|
|
82
82
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const reused = findExistingTunnel(ctx);
|
|
89
|
-
if (reused) {
|
|
90
|
-
ctx = reused;
|
|
85
|
+
if (config.devMode) {
|
|
86
|
+
// Dev mode: local backend can reach localhost directly — no tunnel needed.
|
|
87
|
+
logger.info(`trigger_crawl: dev mode — using localhost URL directly: ${ctx.originalUrl}`);
|
|
91
88
|
}
|
|
92
89
|
else {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
tunnel = await client.tunnels.provisionWithRetry();
|
|
90
|
+
if (progressCallback) {
|
|
91
|
+
await progressCallback({ progress: 1, total: 4, message: 'Provisioning secure tunnel for localhost...' });
|
|
96
92
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}. ` +
|
|
101
|
-
`The remote browser needs a secure tunnel to reach your local dev server. ` +
|
|
102
|
-
`(Detail: ${msg})${diag}`);
|
|
93
|
+
const reused = findExistingTunnel(ctx);
|
|
94
|
+
if (reused) {
|
|
95
|
+
ctx = reused;
|
|
103
96
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
97
|
+
else {
|
|
98
|
+
let tunnel;
|
|
99
|
+
try {
|
|
100
|
+
tunnel = await client.tunnels.provisionWithRetry();
|
|
101
|
+
}
|
|
102
|
+
catch (provisionError) {
|
|
103
|
+
const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
|
|
104
|
+
const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
|
|
105
|
+
throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}. ` +
|
|
106
|
+
`The remote browser needs a secure tunnel to reach your local dev server. ` +
|
|
107
|
+
`(Detail: ${msg})${diag}`);
|
|
108
|
+
}
|
|
109
|
+
keyId = tunnel.keyId;
|
|
110
|
+
ctx = await ensureTunnel(ctx, tunnel.tunnelKey, tunnel.tunnelId, tunnel.keyId, () => client.revokeNgrokKey(tunnel.keyId));
|
|
111
|
+
}
|
|
112
|
+
// Bead 1om: post-tunnel health check — verify traffic actually flows.
|
|
113
|
+
if (ctx.targetUrl) {
|
|
114
|
+
const health = await probeTunnelHealth(ctx.targetUrl);
|
|
115
|
+
if (!health.healthy) {
|
|
116
|
+
const payload = {
|
|
117
|
+
error: 'TunnelTrafficBlocked',
|
|
118
|
+
message: `Tunnel was established but traffic isn't reaching the dev server. ${health.detail ?? ''} Common causes: dev server binds to 0.0.0.0 or ::1 but not 127.0.0.1; dev server crashed; firewall.`,
|
|
119
|
+
detail: {
|
|
120
|
+
code: health.code,
|
|
121
|
+
status: health.status,
|
|
122
|
+
ngrokErrorCode: health.ngrokErrorCode,
|
|
123
|
+
elapsedMs: health.elapsedMs,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
logger.warn(`Tunnel health probe failed for ${ctx.targetUrl}: ${health.code} ${health.ngrokErrorCode ?? ''} in ${health.elapsedMs}ms`);
|
|
127
|
+
if (ctx.tunnelId) {
|
|
128
|
+
tunnelManager.stopTunnel(ctx.tunnelId).catch((err) => logger.warn(`Failed to stop broken tunnel ${ctx.tunnelId}: ${err}`));
|
|
129
|
+
}
|
|
130
|
+
keyId = undefined;
|
|
131
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
124
132
|
}
|
|
125
|
-
keyId = undefined;
|
|
126
|
-
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
}
|
|
@@ -29,7 +29,23 @@ export const createWorkflowsService = (tx) => {
|
|
|
29
29
|
return match;
|
|
30
30
|
},
|
|
31
31
|
async findEvaluationTemplate() {
|
|
32
|
-
|
|
32
|
+
// Try keywords in priority order — allows prod ('app evaluation') and
|
|
33
|
+
// dev backends with different naming ('Browser Use Evaluation Workflow Template')
|
|
34
|
+
// to both resolve without config changes. 'evaluation workflow' is specific
|
|
35
|
+
// enough to exclude 'Browser Use Evaluation Brain'.
|
|
36
|
+
const envOverride = process.env.DEBUGGAI_EVAL_TEMPLATE;
|
|
37
|
+
const keywords = envOverride
|
|
38
|
+
? [envOverride]
|
|
39
|
+
: ['app evaluation', 'evaluation workflow'];
|
|
40
|
+
for (const keyword of keywords) {
|
|
41
|
+
try {
|
|
42
|
+
return await service.findTemplateByName(keyword);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// keyword not matched on this backend, try next
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
33
49
|
},
|
|
34
50
|
async executeWorkflow(workflowUuid, contextData, env) {
|
|
35
51
|
const body = { contextData };
|