browser-debug-mcp-bridge 1.9.0 → 1.11.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 +271 -188
- package/apps/mcp-server/dist/db/automation-repository.js +199 -0
- package/apps/mcp-server/dist/db/automation-repository.js.map +1 -0
- package/apps/mcp-server/dist/db/connection.js +1 -5
- package/apps/mcp-server/dist/db/connection.js.map +1 -1
- package/apps/mcp-server/dist/db/events-repository.js +79 -10
- package/apps/mcp-server/dist/db/events-repository.js.map +1 -1
- package/apps/mcp-server/dist/db/index.js +2 -0
- package/apps/mcp-server/dist/db/index.js.map +1 -1
- package/apps/mcp-server/dist/db/migrations.js +493 -0
- package/apps/mcp-server/dist/db/migrations.js.map +1 -1
- package/apps/mcp-server/dist/db/schema.js +192 -1
- package/apps/mcp-server/dist/db/schema.js.map +1 -1
- package/apps/mcp-server/dist/document-response-rewriter.js +196 -0
- package/apps/mcp-server/dist/document-response-rewriter.js.map +1 -0
- package/apps/mcp-server/dist/json-rewrite.js +189 -0
- package/apps/mcp-server/dist/json-rewrite.js.map +1 -0
- package/apps/mcp-server/dist/main.js +375 -4
- package/apps/mcp-server/dist/main.js.map +1 -1
- package/apps/mcp-server/dist/mcp/server.js +4168 -310
- package/apps/mcp-server/dist/mcp/server.js.map +1 -1
- package/apps/mcp-server/dist/mcp-bridge.js +46 -3
- package/apps/mcp-server/dist/mcp-bridge.js.map +1 -1
- package/apps/mcp-server/dist/next-asset-mapper.js +701 -0
- package/apps/mcp-server/dist/next-asset-mapper.js.map +1 -0
- package/apps/mcp-server/dist/next-source-override-planner.js +601 -0
- package/apps/mcp-server/dist/next-source-override-planner.js.map +1 -0
- package/apps/mcp-server/dist/override-audit-contract.js +51 -0
- package/apps/mcp-server/dist/override-audit-contract.js.map +1 -0
- package/apps/mcp-server/dist/override-audit.js +740 -0
- package/apps/mcp-server/dist/override-audit.js.map +1 -0
- package/apps/mcp-server/dist/override-capabilities.js +136 -0
- package/apps/mcp-server/dist/override-capabilities.js.map +1 -0
- package/apps/mcp-server/dist/override-observed-assets.js +179 -0
- package/apps/mcp-server/dist/override-observed-assets.js.map +1 -0
- package/apps/mcp-server/dist/override-poc.js +336 -0
- package/apps/mcp-server/dist/override-poc.js.map +1 -0
- package/apps/mcp-server/dist/override-profile-generator.js +403 -0
- package/apps/mcp-server/dist/override-profile-generator.js.map +1 -0
- package/apps/mcp-server/dist/override-response-planner.js +557 -0
- package/apps/mcp-server/dist/override-response-planner.js.map +1 -0
- package/apps/mcp-server/dist/override-rule-types.js +32 -0
- package/apps/mcp-server/dist/override-rule-types.js.map +1 -0
- package/apps/mcp-server/dist/retention.js +4 -3
- package/apps/mcp-server/dist/retention.js.map +1 -1
- package/apps/mcp-server/dist/rsc-flight-patch-safety.js +269 -0
- package/apps/mcp-server/dist/rsc-flight-patch-safety.js.map +1 -0
- package/apps/mcp-server/dist/runtime-paths.js +33 -0
- package/apps/mcp-server/dist/runtime-paths.js.map +1 -0
- package/apps/mcp-server/dist/websocket/messages.js +13 -0
- package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
- package/apps/mcp-server/dist/websocket/websocket-server.js +10 -0
- package/apps/mcp-server/dist/websocket/websocket-server.js.map +1 -1
- package/apps/mcp-server/package.json +1 -0
- package/package.json +16 -1
- package/scripts/mcp-start.cjs +201 -11
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
-
import {
|
|
4
|
+
import { createHash, randomUUID } from 'crypto';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
5
6
|
import { dirname, resolve } from 'path';
|
|
7
|
+
import { z } from 'zod';
|
|
6
8
|
import { getConnection } from '../db/connection.js';
|
|
9
|
+
import { diagnoseOverridePoc, insertOverridePlanAudit, listOverridePlanAudits, listOverridePocRequests, listOverridePocRuns, } from '../override-audit.js';
|
|
10
|
+
import { createOverrideProfileConfig, OVERRIDE_PROFILE_ADAPTERS, } from '../override-profile-generator.js';
|
|
11
|
+
import { assertOverrideResponseRequestCaptureSafe, classifyOverrideResponseRequestCapability, } from '../override-capabilities.js';
|
|
12
|
+
import { getOverridePocConfigSummary } from '../override-poc.js';
|
|
13
|
+
import { normalizeOverrideRequestMethod } from '../override-rule-types.js';
|
|
14
|
+
import { mapNextOverrideAssetsWithDrift } from '../next-asset-mapper.js';
|
|
15
|
+
import { planNextSourceOverride } from '../next-source-override-planner.js';
|
|
16
|
+
import { listObservedOverrideAssets, persistObservedOverrideAssets } from '../override-observed-assets.js';
|
|
17
|
+
import { planOverrideResponsePatch } from '../override-response-planner.js';
|
|
7
18
|
function createDefaultMcpLogger() {
|
|
8
19
|
const write = (level, message, payload) => {
|
|
9
20
|
process.stderr.write(`${message} ${JSON.stringify({ level, ...payload })}\n`);
|
|
@@ -20,6 +31,238 @@ function createDefaultMcpLogger() {
|
|
|
20
31
|
},
|
|
21
32
|
};
|
|
22
33
|
}
|
|
34
|
+
const LiveUIActionTargetSchema = z.object({
|
|
35
|
+
selector: z.string().min(1).optional(),
|
|
36
|
+
elementRef: z.string().min(1).optional(),
|
|
37
|
+
tabId: z.number().int().min(0).optional(),
|
|
38
|
+
frameId: z.number().int().min(0).optional(),
|
|
39
|
+
url: z.string().url().optional(),
|
|
40
|
+
});
|
|
41
|
+
const LiveUIActionBaseSchema = z.object({
|
|
42
|
+
traceId: z.string().min(1).optional(),
|
|
43
|
+
target: LiveUIActionTargetSchema.optional(),
|
|
44
|
+
});
|
|
45
|
+
const LiveUIActionRequestSchema = z.discriminatedUnion('action', [
|
|
46
|
+
LiveUIActionBaseSchema.extend({
|
|
47
|
+
action: z.literal('click'),
|
|
48
|
+
input: z.object({
|
|
49
|
+
button: z.enum(['left', 'middle', 'right']).optional(),
|
|
50
|
+
clickCount: z.number().int().min(1).max(3).optional(),
|
|
51
|
+
}).optional(),
|
|
52
|
+
}),
|
|
53
|
+
LiveUIActionBaseSchema.extend({
|
|
54
|
+
action: z.literal('input'),
|
|
55
|
+
input: z.object({
|
|
56
|
+
value: z.string(),
|
|
57
|
+
}),
|
|
58
|
+
}),
|
|
59
|
+
LiveUIActionBaseSchema.extend({
|
|
60
|
+
action: z.literal('focus'),
|
|
61
|
+
input: z.object({}).optional(),
|
|
62
|
+
}),
|
|
63
|
+
LiveUIActionBaseSchema.extend({
|
|
64
|
+
action: z.literal('blur'),
|
|
65
|
+
input: z.object({}).optional(),
|
|
66
|
+
}),
|
|
67
|
+
LiveUIActionBaseSchema.extend({
|
|
68
|
+
action: z.literal('scroll'),
|
|
69
|
+
input: z.object({
|
|
70
|
+
x: z.number().optional(),
|
|
71
|
+
y: z.number().optional(),
|
|
72
|
+
behavior: z.enum(['auto', 'smooth']).optional(),
|
|
73
|
+
}).optional(),
|
|
74
|
+
}),
|
|
75
|
+
LiveUIActionBaseSchema.extend({
|
|
76
|
+
action: z.literal('press_key'),
|
|
77
|
+
input: z.object({
|
|
78
|
+
key: z.string().min(1),
|
|
79
|
+
altKey: z.boolean().optional(),
|
|
80
|
+
ctrlKey: z.boolean().optional(),
|
|
81
|
+
metaKey: z.boolean().optional(),
|
|
82
|
+
shiftKey: z.boolean().optional(),
|
|
83
|
+
}),
|
|
84
|
+
}),
|
|
85
|
+
LiveUIActionBaseSchema.extend({
|
|
86
|
+
action: z.literal('submit'),
|
|
87
|
+
input: z.object({}).optional(),
|
|
88
|
+
}),
|
|
89
|
+
LiveUIActionBaseSchema.extend({
|
|
90
|
+
action: z.literal('reload'),
|
|
91
|
+
input: z.object({
|
|
92
|
+
ignoreCache: z.boolean().optional(),
|
|
93
|
+
}).optional(),
|
|
94
|
+
}),
|
|
95
|
+
]);
|
|
96
|
+
const UIWorkflowModeSchema = z.enum(['safe', 'fast']);
|
|
97
|
+
const UIWorkflowFailureStrategySchema = z.enum(['stop', 'continue', 'retry_once']);
|
|
98
|
+
const UIWorkflowActionTargetScopeSchema = z.enum(['buttons', 'inputs', 'modals', 'focused']);
|
|
99
|
+
const UIWorkflowActionTargetSchema = z.object({
|
|
100
|
+
selector: z.string().min(1).optional(),
|
|
101
|
+
elementRef: z.string().min(1).optional(),
|
|
102
|
+
tabId: z.number().int().min(0).optional(),
|
|
103
|
+
frameId: z.number().int().min(0).optional(),
|
|
104
|
+
url: z.string().url().optional(),
|
|
105
|
+
testId: z.string().min(1).optional(),
|
|
106
|
+
scope: UIWorkflowActionTargetScopeSchema.optional(),
|
|
107
|
+
textContains: z.string().min(1).optional(),
|
|
108
|
+
labelContains: z.string().min(1).optional(),
|
|
109
|
+
titleContains: z.string().min(1).optional(),
|
|
110
|
+
tagName: z.string().min(1).optional(),
|
|
111
|
+
type: z.string().min(1).optional(),
|
|
112
|
+
disabled: z.boolean().optional(),
|
|
113
|
+
selected: z.boolean().optional(),
|
|
114
|
+
pressed: z.boolean().optional(),
|
|
115
|
+
expanded: z.boolean().optional(),
|
|
116
|
+
readOnly: z.boolean().optional(),
|
|
117
|
+
requiredField: z.boolean().optional(),
|
|
118
|
+
}).superRefine((value, ctx) => {
|
|
119
|
+
if (!value.selector
|
|
120
|
+
&& !value.elementRef
|
|
121
|
+
&& !value.testId
|
|
122
|
+
&& !value.textContains
|
|
123
|
+
&& !value.labelContains
|
|
124
|
+
&& !value.titleContains) {
|
|
125
|
+
ctx.addIssue({
|
|
126
|
+
code: z.ZodIssueCode.custom,
|
|
127
|
+
message: 'target requires selector, elementRef, testId, textContains, labelContains, or titleContains',
|
|
128
|
+
path: ['target'],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
const UIWorkflowFailureCaptureSchema = z.object({
|
|
133
|
+
enabled: z.boolean().optional(),
|
|
134
|
+
selector: z.string().min(1).optional(),
|
|
135
|
+
mode: z.enum(['dom', 'png', 'both']).optional(),
|
|
136
|
+
styleMode: z.enum(['computed-lite', 'computed-full']).optional(),
|
|
137
|
+
maxDepth: z.number().int().min(1).max(10).optional(),
|
|
138
|
+
maxBytes: z.number().int().min(1_000).max(200_000).optional(),
|
|
139
|
+
maxAncestors: z.number().int().min(0).max(10).optional(),
|
|
140
|
+
includeDom: z.boolean().optional(),
|
|
141
|
+
includeStyles: z.boolean().optional(),
|
|
142
|
+
includePngDataUrl: z.boolean().optional(),
|
|
143
|
+
});
|
|
144
|
+
const UIWorkflowFailurePolicySchema = z.object({
|
|
145
|
+
strategy: UIWorkflowFailureStrategySchema.optional(),
|
|
146
|
+
capture: UIWorkflowFailureCaptureSchema.optional(),
|
|
147
|
+
});
|
|
148
|
+
const UIWorkflowStepBaseSchema = z.object({
|
|
149
|
+
id: z.string().min(1).optional(),
|
|
150
|
+
note: z.string().min(1).optional(),
|
|
151
|
+
onFailure: UIWorkflowFailurePolicySchema.optional(),
|
|
152
|
+
});
|
|
153
|
+
const UIWorkflowActionBaseSchema = UIWorkflowStepBaseSchema.extend({
|
|
154
|
+
kind: z.literal('action'),
|
|
155
|
+
traceId: z.string().min(1).optional(),
|
|
156
|
+
target: UIWorkflowActionTargetSchema.optional(),
|
|
157
|
+
});
|
|
158
|
+
const UIWorkflowActionStepSchema = z.discriminatedUnion('action', [
|
|
159
|
+
UIWorkflowActionBaseSchema.extend({
|
|
160
|
+
action: z.literal('click'),
|
|
161
|
+
input: z.object({
|
|
162
|
+
button: z.enum(['left', 'middle', 'right']).optional(),
|
|
163
|
+
clickCount: z.number().int().min(1).max(3).optional(),
|
|
164
|
+
}).optional(),
|
|
165
|
+
}),
|
|
166
|
+
UIWorkflowActionBaseSchema.extend({
|
|
167
|
+
action: z.literal('input'),
|
|
168
|
+
input: z.object({
|
|
169
|
+
value: z.string(),
|
|
170
|
+
}),
|
|
171
|
+
}),
|
|
172
|
+
UIWorkflowActionBaseSchema.extend({
|
|
173
|
+
action: z.literal('focus'),
|
|
174
|
+
input: z.object({}).optional(),
|
|
175
|
+
}),
|
|
176
|
+
UIWorkflowActionBaseSchema.extend({
|
|
177
|
+
action: z.literal('blur'),
|
|
178
|
+
input: z.object({}).optional(),
|
|
179
|
+
}),
|
|
180
|
+
UIWorkflowActionBaseSchema.extend({
|
|
181
|
+
action: z.literal('scroll'),
|
|
182
|
+
input: z.object({
|
|
183
|
+
x: z.number().optional(),
|
|
184
|
+
y: z.number().optional(),
|
|
185
|
+
behavior: z.enum(['auto', 'smooth']).optional(),
|
|
186
|
+
}).optional(),
|
|
187
|
+
}),
|
|
188
|
+
UIWorkflowActionBaseSchema.extend({
|
|
189
|
+
action: z.literal('press_key'),
|
|
190
|
+
input: z.object({
|
|
191
|
+
key: z.string().min(1),
|
|
192
|
+
altKey: z.boolean().optional(),
|
|
193
|
+
ctrlKey: z.boolean().optional(),
|
|
194
|
+
metaKey: z.boolean().optional(),
|
|
195
|
+
shiftKey: z.boolean().optional(),
|
|
196
|
+
}),
|
|
197
|
+
}),
|
|
198
|
+
UIWorkflowActionBaseSchema.extend({
|
|
199
|
+
action: z.literal('submit'),
|
|
200
|
+
input: z.object({}).optional(),
|
|
201
|
+
}),
|
|
202
|
+
UIWorkflowActionBaseSchema.extend({
|
|
203
|
+
action: z.literal('reload'),
|
|
204
|
+
input: z.object({
|
|
205
|
+
ignoreCache: z.boolean().optional(),
|
|
206
|
+
}).optional(),
|
|
207
|
+
}),
|
|
208
|
+
]);
|
|
209
|
+
const UIWorkflowPageStateMatcherSchema = z.object({
|
|
210
|
+
scope: z.enum(['buttons', 'inputs', 'modals', 'focused', 'page']),
|
|
211
|
+
selector: z.string().optional(),
|
|
212
|
+
testId: z.string().optional(),
|
|
213
|
+
textContains: z.string().optional(),
|
|
214
|
+
labelContains: z.string().optional(),
|
|
215
|
+
titleContains: z.string().optional(),
|
|
216
|
+
urlContains: z.string().optional(),
|
|
217
|
+
language: z.string().optional(),
|
|
218
|
+
disabled: z.boolean().optional(),
|
|
219
|
+
selected: z.boolean().optional(),
|
|
220
|
+
pressed: z.boolean().optional(),
|
|
221
|
+
expanded: z.boolean().optional(),
|
|
222
|
+
readOnly: z.boolean().optional(),
|
|
223
|
+
requiredField: z.boolean().optional(),
|
|
224
|
+
tagName: z.string().optional(),
|
|
225
|
+
type: z.string().optional(),
|
|
226
|
+
countExactly: z.number().int().min(0).optional(),
|
|
227
|
+
countAtLeast: z.number().int().min(0).optional(),
|
|
228
|
+
maxItems: z.number().int().min(1).max(100).optional(),
|
|
229
|
+
maxTextLength: z.number().int().min(8).max(200).optional(),
|
|
230
|
+
}).superRefine((value, ctx) => {
|
|
231
|
+
if (value.countExactly !== undefined && value.countAtLeast !== undefined) {
|
|
232
|
+
ctx.addIssue({
|
|
233
|
+
code: z.ZodIssueCode.custom,
|
|
234
|
+
message: 'countExactly and countAtLeast cannot both be set',
|
|
235
|
+
path: ['countExactly'],
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
const UIWorkflowWaitForStepSchema = UIWorkflowStepBaseSchema.extend({
|
|
240
|
+
kind: z.literal('waitFor'),
|
|
241
|
+
matcher: UIWorkflowPageStateMatcherSchema.extend({
|
|
242
|
+
timeoutMs: z.number().int().min(100).max(30000).optional(),
|
|
243
|
+
pollIntervalMs: z.number().int().min(50).max(2000).optional(),
|
|
244
|
+
}),
|
|
245
|
+
});
|
|
246
|
+
const UIWorkflowAssertStepSchema = UIWorkflowStepBaseSchema.extend({
|
|
247
|
+
kind: z.literal('assert'),
|
|
248
|
+
matcher: UIWorkflowPageStateMatcherSchema,
|
|
249
|
+
});
|
|
250
|
+
const UIWorkflowStepSchema = z.discriminatedUnion('kind', [
|
|
251
|
+
UIWorkflowActionStepSchema,
|
|
252
|
+
UIWorkflowWaitForStepSchema,
|
|
253
|
+
UIWorkflowAssertStepSchema,
|
|
254
|
+
]);
|
|
255
|
+
const RunUIStepsSchema = z.object({
|
|
256
|
+
sessionId: z.string().min(1),
|
|
257
|
+
mode: UIWorkflowModeSchema.default('safe'),
|
|
258
|
+
stopOnFailure: z.boolean().default(true),
|
|
259
|
+
defaultTimeoutMs: z.number().int().min(100).max(30000).optional(),
|
|
260
|
+
defaultPollIntervalMs: z.number().int().min(50).max(2000).optional(),
|
|
261
|
+
steps: z.array(UIWorkflowStepSchema).min(1).max(50),
|
|
262
|
+
});
|
|
263
|
+
function createUIWorkflowTraceId() {
|
|
264
|
+
return `uiworkflow-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
265
|
+
}
|
|
23
266
|
const TOOL_SCHEMAS = {
|
|
24
267
|
list_sessions: {
|
|
25
268
|
type: 'object',
|
|
@@ -37,6 +280,13 @@ const TOOL_SCHEMAS = {
|
|
|
37
280
|
sessionId: { type: 'string' },
|
|
38
281
|
},
|
|
39
282
|
},
|
|
283
|
+
get_live_session_health: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
required: ['sessionId'],
|
|
286
|
+
properties: {
|
|
287
|
+
sessionId: { type: 'string' },
|
|
288
|
+
},
|
|
289
|
+
},
|
|
40
290
|
get_recent_events: {
|
|
41
291
|
type: 'object',
|
|
42
292
|
properties: {
|
|
@@ -212,6 +462,96 @@ const TOOL_SCHEMAS = {
|
|
|
212
462
|
selector: { type: 'string' },
|
|
213
463
|
},
|
|
214
464
|
},
|
|
465
|
+
get_page_state: {
|
|
466
|
+
type: 'object',
|
|
467
|
+
required: ['sessionId'],
|
|
468
|
+
properties: {
|
|
469
|
+
sessionId: { type: 'string' },
|
|
470
|
+
maxItems: { type: 'number' },
|
|
471
|
+
maxTextLength: { type: 'number' },
|
|
472
|
+
includeButtons: { type: 'boolean' },
|
|
473
|
+
includeInputs: { type: 'boolean' },
|
|
474
|
+
includeModals: { type: 'boolean' },
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
get_interactive_elements: {
|
|
478
|
+
type: 'object',
|
|
479
|
+
required: ['sessionId'],
|
|
480
|
+
properties: {
|
|
481
|
+
sessionId: { type: 'string' },
|
|
482
|
+
kinds: {
|
|
483
|
+
type: 'array',
|
|
484
|
+
items: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
|
|
485
|
+
},
|
|
486
|
+
maxItems: { type: 'number' },
|
|
487
|
+
maxTextLength: { type: 'number' },
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
set_viewport: {
|
|
491
|
+
type: 'object',
|
|
492
|
+
required: ['sessionId', 'width', 'height'],
|
|
493
|
+
properties: {
|
|
494
|
+
sessionId: { type: 'string' },
|
|
495
|
+
width: { type: 'number' },
|
|
496
|
+
height: { type: 'number' },
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
assert_page_state: {
|
|
500
|
+
type: 'object',
|
|
501
|
+
required: ['sessionId', 'scope'],
|
|
502
|
+
properties: {
|
|
503
|
+
sessionId: { type: 'string' },
|
|
504
|
+
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
505
|
+
selector: { type: 'string' },
|
|
506
|
+
testId: { type: 'string' },
|
|
507
|
+
textContains: { type: 'string' },
|
|
508
|
+
labelContains: { type: 'string' },
|
|
509
|
+
titleContains: { type: 'string' },
|
|
510
|
+
urlContains: { type: 'string' },
|
|
511
|
+
language: { type: 'string' },
|
|
512
|
+
disabled: { type: 'boolean' },
|
|
513
|
+
selected: { type: 'boolean' },
|
|
514
|
+
pressed: { type: 'boolean' },
|
|
515
|
+
expanded: { type: 'boolean' },
|
|
516
|
+
readOnly: { type: 'boolean' },
|
|
517
|
+
requiredField: { type: 'boolean' },
|
|
518
|
+
tagName: { type: 'string' },
|
|
519
|
+
type: { type: 'string' },
|
|
520
|
+
countExactly: { type: 'number' },
|
|
521
|
+
countAtLeast: { type: 'number' },
|
|
522
|
+
maxItems: { type: 'number' },
|
|
523
|
+
maxTextLength: { type: 'number' },
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
wait_for_page_state: {
|
|
527
|
+
type: 'object',
|
|
528
|
+
required: ['sessionId', 'scope'],
|
|
529
|
+
properties: {
|
|
530
|
+
sessionId: { type: 'string' },
|
|
531
|
+
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
532
|
+
selector: { type: 'string' },
|
|
533
|
+
testId: { type: 'string' },
|
|
534
|
+
textContains: { type: 'string' },
|
|
535
|
+
labelContains: { type: 'string' },
|
|
536
|
+
titleContains: { type: 'string' },
|
|
537
|
+
urlContains: { type: 'string' },
|
|
538
|
+
language: { type: 'string' },
|
|
539
|
+
disabled: { type: 'boolean' },
|
|
540
|
+
selected: { type: 'boolean' },
|
|
541
|
+
pressed: { type: 'boolean' },
|
|
542
|
+
expanded: { type: 'boolean' },
|
|
543
|
+
readOnly: { type: 'boolean' },
|
|
544
|
+
requiredField: { type: 'boolean' },
|
|
545
|
+
tagName: { type: 'string' },
|
|
546
|
+
type: { type: 'string' },
|
|
547
|
+
countExactly: { type: 'number' },
|
|
548
|
+
countAtLeast: { type: 'number' },
|
|
549
|
+
maxItems: { type: 'number' },
|
|
550
|
+
maxTextLength: { type: 'number' },
|
|
551
|
+
timeoutMs: { type: 'number' },
|
|
552
|
+
pollIntervalMs: { type: 'number' },
|
|
553
|
+
},
|
|
554
|
+
},
|
|
215
555
|
capture_ui_snapshot: {
|
|
216
556
|
type: 'object',
|
|
217
557
|
required: ['sessionId'],
|
|
@@ -247,6 +587,223 @@ const TOOL_SCHEMAS = {
|
|
|
247
587
|
maxResponseBytes: { type: 'number' },
|
|
248
588
|
},
|
|
249
589
|
},
|
|
590
|
+
list_override_profiles: {
|
|
591
|
+
type: 'object',
|
|
592
|
+
properties: {},
|
|
593
|
+
},
|
|
594
|
+
create_override_profile: {
|
|
595
|
+
type: 'object',
|
|
596
|
+
required: ['targetBaseUrl'],
|
|
597
|
+
properties: {
|
|
598
|
+
adapter: { type: 'string' },
|
|
599
|
+
mode: { type: 'string' },
|
|
600
|
+
targetBaseUrl: { type: 'string' },
|
|
601
|
+
projectRoot: { type: 'string' },
|
|
602
|
+
assetRoot: { type: 'string' },
|
|
603
|
+
nextDir: { type: 'string' },
|
|
604
|
+
configPath: { type: 'string' },
|
|
605
|
+
profileId: { type: 'string' },
|
|
606
|
+
profileName: { type: 'string' },
|
|
607
|
+
enabled: { type: 'boolean' },
|
|
608
|
+
profileEnabled: { type: 'boolean' },
|
|
609
|
+
autoReload: { type: 'boolean' },
|
|
610
|
+
includeManifestFiles: { type: 'boolean' },
|
|
611
|
+
includeStaticFiles: { type: 'boolean' },
|
|
612
|
+
extensions: { type: 'array', items: { type: 'string' } },
|
|
613
|
+
maxRules: { type: 'number' },
|
|
614
|
+
writeConfig: { type: 'boolean' },
|
|
615
|
+
overwrite: { type: 'boolean' },
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
validate_override_profile: {
|
|
619
|
+
type: 'object',
|
|
620
|
+
properties: {
|
|
621
|
+
profileId: { type: 'string' },
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
preflight_overrides: {
|
|
625
|
+
type: 'object',
|
|
626
|
+
required: ['sessionId'],
|
|
627
|
+
properties: {
|
|
628
|
+
sessionId: { type: 'string' },
|
|
629
|
+
profileId: { type: 'string' },
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
observe_override_assets: {
|
|
633
|
+
type: 'object',
|
|
634
|
+
required: ['sessionId'],
|
|
635
|
+
properties: {
|
|
636
|
+
sessionId: { type: 'string' },
|
|
637
|
+
tabId: { type: 'number' },
|
|
638
|
+
includePerformance: { type: 'boolean' },
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
capture_override_response_body: {
|
|
642
|
+
type: 'object',
|
|
643
|
+
required: ['sessionId'],
|
|
644
|
+
properties: {
|
|
645
|
+
sessionId: { type: 'string' },
|
|
646
|
+
tabId: { type: 'number' },
|
|
647
|
+
targetUrl: { type: 'string' },
|
|
648
|
+
targetAssetUrl: { type: 'string' },
|
|
649
|
+
captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
|
|
650
|
+
triggerReload: { type: 'boolean' },
|
|
651
|
+
matchMode: { type: 'string', enum: ['exact', 'prefix'] },
|
|
652
|
+
requestMethod: { type: 'string' },
|
|
653
|
+
requestHeaders: { type: 'object' },
|
|
654
|
+
timeoutMs: { type: 'number' },
|
|
655
|
+
maxBodyBytes: { type: 'number' },
|
|
656
|
+
includeBody: { type: 'boolean' },
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
list_observed_override_assets: {
|
|
660
|
+
type: 'object',
|
|
661
|
+
required: ['sessionId'],
|
|
662
|
+
properties: {
|
|
663
|
+
sessionId: { type: 'string' },
|
|
664
|
+
limit: { type: 'number' },
|
|
665
|
+
sinceTimestamp: { type: 'number' },
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
map_next_override_assets: {
|
|
669
|
+
type: 'object',
|
|
670
|
+
required: ['projectRoot'],
|
|
671
|
+
properties: {
|
|
672
|
+
sessionId: { type: 'string' },
|
|
673
|
+
tabId: { type: 'number' },
|
|
674
|
+
projectRoot: { type: 'string' },
|
|
675
|
+
nextDir: { type: 'string' },
|
|
676
|
+
route: { type: 'string' },
|
|
677
|
+
sourcePaths: { type: 'array', items: { type: 'string' } },
|
|
678
|
+
observedAssets: { type: 'array', items: { type: 'object' } },
|
|
679
|
+
maxResults: { type: 'number' },
|
|
680
|
+
fetchProductionAssets: { type: 'boolean' },
|
|
681
|
+
productionFetchTimeoutMs: { type: 'number' },
|
|
682
|
+
maxProductionAssetBytes: { type: 'number' },
|
|
683
|
+
maxDriftCandidates: { type: 'number' },
|
|
684
|
+
productionFetchConcurrency: { type: 'number' },
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
plan_override_response_patch: {
|
|
688
|
+
type: 'object',
|
|
689
|
+
properties: {
|
|
690
|
+
sessionId: { type: 'string' },
|
|
691
|
+
tabId: { type: 'number' },
|
|
692
|
+
targetUrl: { type: 'string' },
|
|
693
|
+
targetAssetUrl: { type: 'string' },
|
|
694
|
+
captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
|
|
695
|
+
triggerReload: { type: 'boolean' },
|
|
696
|
+
ruleType: { type: 'string' },
|
|
697
|
+
requestMethod: { type: 'string' },
|
|
698
|
+
matchMode: { type: 'string', enum: ['exact', 'prefix'] },
|
|
699
|
+
requestHeaders: { type: 'object' },
|
|
700
|
+
timeoutMs: { type: 'number' },
|
|
701
|
+
contentType: { type: 'string' },
|
|
702
|
+
responseBodyText: { type: 'string' },
|
|
703
|
+
bodyText: { type: 'string' },
|
|
704
|
+
responseBodyBase64: { type: 'string' },
|
|
705
|
+
bodyBase64: { type: 'string' },
|
|
706
|
+
textPatches: { type: 'array', items: { type: 'object' } },
|
|
707
|
+
jsonPatches: { type: 'array', items: { type: 'object' } },
|
|
708
|
+
documentPatches: { type: 'array', items: { type: 'object' } },
|
|
709
|
+
maxBodyBytes: { type: 'number' },
|
|
710
|
+
outputRoot: { type: 'string' },
|
|
711
|
+
configPath: { type: 'string' },
|
|
712
|
+
writeBody: { type: 'boolean' },
|
|
713
|
+
writeConfig: { type: 'boolean' },
|
|
714
|
+
overwrite: { type: 'boolean' },
|
|
715
|
+
enabled: { type: 'boolean' },
|
|
716
|
+
profileEnabled: { type: 'boolean' },
|
|
717
|
+
autoReload: { type: 'boolean' },
|
|
718
|
+
profileId: { type: 'string' },
|
|
719
|
+
profileName: { type: 'string' },
|
|
720
|
+
ruleId: { type: 'string' },
|
|
721
|
+
includePreview: { type: 'boolean' },
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
plan_next_source_override: {
|
|
725
|
+
type: 'object',
|
|
726
|
+
required: ['projectRoot', 'sourceEdits'],
|
|
727
|
+
properties: {
|
|
728
|
+
sessionId: { type: 'string' },
|
|
729
|
+
tabId: { type: 'number' },
|
|
730
|
+
projectRoot: { type: 'string' },
|
|
731
|
+
nextDir: { type: 'string' },
|
|
732
|
+
route: { type: 'string' },
|
|
733
|
+
sourcePaths: { type: 'array', items: { type: 'string' } },
|
|
734
|
+
sourceEdits: { type: 'array', items: { type: 'object' } },
|
|
735
|
+
observedAssets: { type: 'array', items: { type: 'object' } },
|
|
736
|
+
configPath: { type: 'string' },
|
|
737
|
+
writeConfig: { type: 'boolean' },
|
|
738
|
+
overwrite: { type: 'boolean' },
|
|
739
|
+
enabled: { type: 'boolean' },
|
|
740
|
+
profileEnabled: { type: 'boolean' },
|
|
741
|
+
autoReload: { type: 'boolean' },
|
|
742
|
+
profileId: { type: 'string' },
|
|
743
|
+
profileName: { type: 'string' },
|
|
744
|
+
buildTimeoutMs: { type: 'number' },
|
|
745
|
+
maxRules: { type: 'number' },
|
|
746
|
+
fetchProductionAssets: { type: 'boolean' },
|
|
747
|
+
productionFetchTimeoutMs: { type: 'number' },
|
|
748
|
+
maxProductionAssetBytes: { type: 'number' },
|
|
749
|
+
maxDriftCandidates: { type: 'number' },
|
|
750
|
+
productionFetchConcurrency: { type: 'number' },
|
|
751
|
+
overlayTtlMs: { type: 'number' },
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
enable_overrides: {
|
|
755
|
+
type: 'object',
|
|
756
|
+
required: ['sessionId'],
|
|
757
|
+
properties: {
|
|
758
|
+
sessionId: { type: 'string' },
|
|
759
|
+
tabId: { type: 'number' },
|
|
760
|
+
profileId: { type: 'string' },
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
disable_overrides: {
|
|
764
|
+
type: 'object',
|
|
765
|
+
required: ['sessionId'],
|
|
766
|
+
properties: {
|
|
767
|
+
sessionId: { type: 'string' },
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
get_override_status: {
|
|
771
|
+
type: 'object',
|
|
772
|
+
properties: {
|
|
773
|
+
sessionId: { type: 'string' },
|
|
774
|
+
profileId: { type: 'string' },
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
get_override_request_log: {
|
|
778
|
+
type: 'object',
|
|
779
|
+
required: ['sessionId'],
|
|
780
|
+
properties: {
|
|
781
|
+
sessionId: { type: 'string' },
|
|
782
|
+
runId: { type: 'string' },
|
|
783
|
+
limit: { type: 'number' },
|
|
784
|
+
offset: { type: 'number' },
|
|
785
|
+
maxResponseBytes: { type: 'number' },
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
get_override_plan_log: {
|
|
789
|
+
type: 'object',
|
|
790
|
+
required: ['sessionId'],
|
|
791
|
+
properties: {
|
|
792
|
+
sessionId: { type: 'string' },
|
|
793
|
+
planId: { type: 'string' },
|
|
794
|
+
limit: { type: 'number' },
|
|
795
|
+
offset: { type: 'number' },
|
|
796
|
+
maxResponseBytes: { type: 'number' },
|
|
797
|
+
},
|
|
798
|
+
},
|
|
799
|
+
diagnose_overrides: {
|
|
800
|
+
type: 'object',
|
|
801
|
+
required: ['sessionId'],
|
|
802
|
+
properties: {
|
|
803
|
+
sessionId: { type: 'string' },
|
|
804
|
+
runId: { type: 'string' },
|
|
805
|
+
},
|
|
806
|
+
},
|
|
250
807
|
explain_last_failure: {
|
|
251
808
|
type: 'object',
|
|
252
809
|
required: ['sessionId'],
|
|
@@ -298,6 +855,191 @@ const TOOL_SCHEMAS = {
|
|
|
298
855
|
encoding: { type: 'string' },
|
|
299
856
|
},
|
|
300
857
|
},
|
|
858
|
+
list_automation_runs: {
|
|
859
|
+
type: 'object',
|
|
860
|
+
required: ['sessionId'],
|
|
861
|
+
properties: {
|
|
862
|
+
sessionId: { type: 'string' },
|
|
863
|
+
status: { type: 'string', enum: ['requested', 'started', 'succeeded', 'failed', 'rejected', 'stopped'] },
|
|
864
|
+
action: { type: 'string', enum: ['click', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
|
|
865
|
+
traceId: { type: 'string' },
|
|
866
|
+
limit: { type: 'number' },
|
|
867
|
+
offset: { type: 'number' },
|
|
868
|
+
maxResponseBytes: { type: 'number' },
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
get_automation_run: {
|
|
872
|
+
type: 'object',
|
|
873
|
+
required: ['sessionId', 'runId'],
|
|
874
|
+
properties: {
|
|
875
|
+
sessionId: { type: 'string' },
|
|
876
|
+
runId: { type: 'string' },
|
|
877
|
+
stepLimit: { type: 'number' },
|
|
878
|
+
stepOffset: { type: 'number' },
|
|
879
|
+
maxResponseBytes: { type: 'number' },
|
|
880
|
+
},
|
|
881
|
+
},
|
|
882
|
+
execute_ui_action: {
|
|
883
|
+
type: 'object',
|
|
884
|
+
required: ['sessionId', 'action'],
|
|
885
|
+
properties: {
|
|
886
|
+
sessionId: { type: 'string' },
|
|
887
|
+
action: { type: 'string', enum: ['click', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
|
|
888
|
+
traceId: { type: 'string' },
|
|
889
|
+
target: {
|
|
890
|
+
type: 'object',
|
|
891
|
+
properties: {
|
|
892
|
+
selector: { type: 'string' },
|
|
893
|
+
elementRef: { type: 'string' },
|
|
894
|
+
tabId: { type: 'number' },
|
|
895
|
+
frameId: { type: 'number' },
|
|
896
|
+
url: { type: 'string' },
|
|
897
|
+
},
|
|
898
|
+
},
|
|
899
|
+
input: { type: 'object' },
|
|
900
|
+
captureOnFailure: {
|
|
901
|
+
type: 'object',
|
|
902
|
+
properties: {
|
|
903
|
+
enabled: { type: 'boolean' },
|
|
904
|
+
selector: { type: 'string' },
|
|
905
|
+
mode: { type: 'string', enum: ['dom', 'png', 'both'] },
|
|
906
|
+
styleMode: { type: 'string', enum: ['computed-lite', 'computed-full'] },
|
|
907
|
+
maxDepth: { type: 'number' },
|
|
908
|
+
maxBytes: { type: 'number' },
|
|
909
|
+
maxAncestors: { type: 'number' },
|
|
910
|
+
includeDom: { type: 'boolean' },
|
|
911
|
+
includeStyles: { type: 'boolean' },
|
|
912
|
+
includePngDataUrl: { type: 'boolean' },
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
waitForPageState: {
|
|
916
|
+
type: 'object',
|
|
917
|
+
required: ['scope'],
|
|
918
|
+
properties: {
|
|
919
|
+
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
920
|
+
selector: { type: 'string' },
|
|
921
|
+
testId: { type: 'string' },
|
|
922
|
+
textContains: { type: 'string' },
|
|
923
|
+
labelContains: { type: 'string' },
|
|
924
|
+
titleContains: { type: 'string' },
|
|
925
|
+
urlContains: { type: 'string' },
|
|
926
|
+
language: { type: 'string' },
|
|
927
|
+
disabled: { type: 'boolean' },
|
|
928
|
+
selected: { type: 'boolean' },
|
|
929
|
+
pressed: { type: 'boolean' },
|
|
930
|
+
expanded: { type: 'boolean' },
|
|
931
|
+
readOnly: { type: 'boolean' },
|
|
932
|
+
requiredField: { type: 'boolean' },
|
|
933
|
+
tagName: { type: 'string' },
|
|
934
|
+
type: { type: 'string' },
|
|
935
|
+
countExactly: { type: 'number' },
|
|
936
|
+
countAtLeast: { type: 'number' },
|
|
937
|
+
maxItems: { type: 'number' },
|
|
938
|
+
maxTextLength: { type: 'number' },
|
|
939
|
+
timeoutMs: { type: 'number' },
|
|
940
|
+
pollIntervalMs: { type: 'number' },
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
run_ui_steps: {
|
|
946
|
+
type: 'object',
|
|
947
|
+
required: ['sessionId', 'steps'],
|
|
948
|
+
properties: {
|
|
949
|
+
sessionId: { type: 'string' },
|
|
950
|
+
mode: { type: 'string', enum: ['safe', 'fast'] },
|
|
951
|
+
stopOnFailure: { type: 'boolean' },
|
|
952
|
+
defaultTimeoutMs: { type: 'number' },
|
|
953
|
+
defaultPollIntervalMs: { type: 'number' },
|
|
954
|
+
steps: {
|
|
955
|
+
type: 'array',
|
|
956
|
+
minItems: 1,
|
|
957
|
+
items: {
|
|
958
|
+
type: 'object',
|
|
959
|
+
required: ['kind'],
|
|
960
|
+
properties: {
|
|
961
|
+
id: { type: 'string' },
|
|
962
|
+
note: { type: 'string' },
|
|
963
|
+
kind: { type: 'string', enum: ['action', 'waitFor', 'assert'] },
|
|
964
|
+
action: { type: 'string' },
|
|
965
|
+
traceId: { type: 'string' },
|
|
966
|
+
target: {
|
|
967
|
+
type: 'object',
|
|
968
|
+
properties: {
|
|
969
|
+
selector: { type: 'string' },
|
|
970
|
+
elementRef: { type: 'string' },
|
|
971
|
+
tabId: { type: 'number' },
|
|
972
|
+
frameId: { type: 'number' },
|
|
973
|
+
url: { type: 'string' },
|
|
974
|
+
testId: { type: 'string' },
|
|
975
|
+
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
|
|
976
|
+
textContains: { type: 'string' },
|
|
977
|
+
labelContains: { type: 'string' },
|
|
978
|
+
titleContains: { type: 'string' },
|
|
979
|
+
tagName: { type: 'string' },
|
|
980
|
+
type: { type: 'string' },
|
|
981
|
+
disabled: { type: 'boolean' },
|
|
982
|
+
selected: { type: 'boolean' },
|
|
983
|
+
pressed: { type: 'boolean' },
|
|
984
|
+
expanded: { type: 'boolean' },
|
|
985
|
+
readOnly: { type: 'boolean' },
|
|
986
|
+
requiredField: { type: 'boolean' },
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
input: { type: 'object' },
|
|
990
|
+
onFailure: {
|
|
991
|
+
type: 'object',
|
|
992
|
+
properties: {
|
|
993
|
+
strategy: { type: 'string', enum: ['stop', 'continue', 'retry_once'] },
|
|
994
|
+
capture: {
|
|
995
|
+
type: 'object',
|
|
996
|
+
properties: {
|
|
997
|
+
enabled: { type: 'boolean' },
|
|
998
|
+
selector: { type: 'string' },
|
|
999
|
+
mode: { type: 'string', enum: ['dom', 'png', 'both'] },
|
|
1000
|
+
styleMode: { type: 'string', enum: ['computed-lite', 'computed-full'] },
|
|
1001
|
+
maxDepth: { type: 'number' },
|
|
1002
|
+
maxBytes: { type: 'number' },
|
|
1003
|
+
maxAncestors: { type: 'number' },
|
|
1004
|
+
includeDom: { type: 'boolean' },
|
|
1005
|
+
includeStyles: { type: 'boolean' },
|
|
1006
|
+
includePngDataUrl: { type: 'boolean' },
|
|
1007
|
+
},
|
|
1008
|
+
},
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
matcher: {
|
|
1012
|
+
type: 'object',
|
|
1013
|
+
properties: {
|
|
1014
|
+
scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
|
|
1015
|
+
selector: { type: 'string' },
|
|
1016
|
+
testId: { type: 'string' },
|
|
1017
|
+
textContains: { type: 'string' },
|
|
1018
|
+
labelContains: { type: 'string' },
|
|
1019
|
+
titleContains: { type: 'string' },
|
|
1020
|
+
urlContains: { type: 'string' },
|
|
1021
|
+
language: { type: 'string' },
|
|
1022
|
+
disabled: { type: 'boolean' },
|
|
1023
|
+
selected: { type: 'boolean' },
|
|
1024
|
+
pressed: { type: 'boolean' },
|
|
1025
|
+
expanded: { type: 'boolean' },
|
|
1026
|
+
readOnly: { type: 'boolean' },
|
|
1027
|
+
requiredField: { type: 'boolean' },
|
|
1028
|
+
tagName: { type: 'string' },
|
|
1029
|
+
type: { type: 'string' },
|
|
1030
|
+
countExactly: { type: 'number' },
|
|
1031
|
+
countAtLeast: { type: 'number' },
|
|
1032
|
+
maxItems: { type: 'number' },
|
|
1033
|
+
maxTextLength: { type: 'number' },
|
|
1034
|
+
timeoutMs: { type: 'number' },
|
|
1035
|
+
pollIntervalMs: { type: 'number' },
|
|
1036
|
+
},
|
|
1037
|
+
},
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
301
1043
|
};
|
|
302
1044
|
const TOOL_DESCRIPTIONS = {
|
|
303
1045
|
list_sessions: 'List captured debugging sessions',
|
|
@@ -318,13 +1060,39 @@ const TOOL_DESCRIPTIONS = {
|
|
|
318
1060
|
get_dom_document: 'Capture full document as outline or html',
|
|
319
1061
|
get_computed_styles: 'Read computed CSS styles for an element',
|
|
320
1062
|
get_layout_metrics: 'Read viewport and element layout metrics',
|
|
1063
|
+
get_page_state: 'Read a compact structured page model for forms, buttons, modals, and viewport state',
|
|
1064
|
+
get_interactive_elements: 'Read compact live element references for buttons, inputs, modals, and focused elements',
|
|
1065
|
+
get_live_session_health: 'Read live transport health and session binding details for one session',
|
|
1066
|
+
set_viewport: 'Resize the live browser window for a session and return the resulting viewport metrics',
|
|
1067
|
+
assert_page_state: 'Assert compact page-state conditions without pulling raw DOM payloads',
|
|
1068
|
+
wait_for_page_state: 'Poll compact page state until a structured assertion becomes true',
|
|
321
1069
|
capture_ui_snapshot: 'Capture redacted UI snapshot (DOM/styles/optional PNG) and persist it',
|
|
322
1070
|
get_live_console_logs: 'Read in-memory live console logs for a connected session',
|
|
1071
|
+
list_override_profiles: 'List configured browser override profiles',
|
|
1072
|
+
create_override_profile: 'Generate a candidate browser override profile from local build assets',
|
|
1073
|
+
validate_override_profile: 'Validate the current browser override profile and local asset readiness',
|
|
1074
|
+
preflight_overrides: 'Run production-safety checks before enabling browser overrides for a live session',
|
|
1075
|
+
observe_override_assets: 'Observe production render artifacts from a live extension tab',
|
|
1076
|
+
capture_override_response_body: 'Capture a bounded text response body from a live extension session for override planning, using extension fetch or explicit CDP response-stage capture',
|
|
1077
|
+
list_observed_override_assets: 'List persisted production render artifacts observed for a session',
|
|
1078
|
+
map_next_override_assets: 'Map observed production Next.js assets to local build chunks and source paths',
|
|
1079
|
+
plan_override_response_patch: 'Patch a supplied or live-captured text response body with literal textPatches or JSON Pointer jsonPatches and write an exact or prefix override rule for supported response types',
|
|
1080
|
+
plan_next_source_override: 'Apply source edits in a temp Next.js overlay build and plan exact browser override rules',
|
|
1081
|
+
enable_overrides: 'Enable browser overrides for a live extension session',
|
|
1082
|
+
disable_overrides: 'Disable browser overrides for a live extension session',
|
|
1083
|
+
get_override_status: 'Read live or persisted browser override status for a session',
|
|
1084
|
+
get_override_request_log: 'Read persisted browser override request audit rows',
|
|
1085
|
+
get_override_plan_log: 'Read persisted generated override plan audit rows with previews, hashes, and rollback metadata',
|
|
1086
|
+
diagnose_overrides: 'Diagnose persisted browser override runs and failure indicators',
|
|
323
1087
|
explain_last_failure: 'Explain the latest failure timeline',
|
|
324
1088
|
get_event_correlation: 'Correlate related events by window',
|
|
325
1089
|
list_snapshots: 'List snapshot metadata by session/time/trigger',
|
|
326
1090
|
get_snapshot_for_event: 'Find snapshot most related to an event',
|
|
327
1091
|
get_snapshot_asset: 'Read bounded binary chunks for snapshot assets',
|
|
1092
|
+
list_automation_runs: 'List first-class automation runs from dedicated automation tables',
|
|
1093
|
+
get_automation_run: 'Inspect one automation run with bounded step details',
|
|
1094
|
+
execute_ui_action: 'Execute one live UI action in the current bound extension session',
|
|
1095
|
+
run_ui_steps: 'Run a small generic UI workflow locally in the bridge using actions, waits, and assertions',
|
|
328
1096
|
};
|
|
329
1097
|
const ALL_TOOLS = Object.keys(TOOL_SCHEMAS);
|
|
330
1098
|
const DEFAULT_REDACTION_SUMMARY = {
|
|
@@ -345,6 +1113,16 @@ const DEFAULT_NETWORK_POLL_TIMEOUT_MS = 15_000;
|
|
|
345
1113
|
const MAX_NETWORK_POLL_TIMEOUT_MS = 120_000;
|
|
346
1114
|
const DEFAULT_NETWORK_POLL_INTERVAL_MS = 250;
|
|
347
1115
|
const LIVE_SESSION_DISCONNECTED_CODE = 'LIVE_SESSION_DISCONNECTED';
|
|
1116
|
+
const STALE_LIVE_CONNECTION_GRACE_WINDOW_MS = 30 * 60 * 1000;
|
|
1117
|
+
const NOISE_SESSION_HOST_PATTERNS = [
|
|
1118
|
+
/(^|\.)adtrafficquality\.google$/i,
|
|
1119
|
+
/(^|\.)doubleclick\.net$/i,
|
|
1120
|
+
/(^|\.)googlesyndication\.com$/i,
|
|
1121
|
+
/(^|\.)googleadservices\.com$/i,
|
|
1122
|
+
/(^|\.)recaptcha\.net$/i,
|
|
1123
|
+
/(^|\.)gstatic\.com$/i,
|
|
1124
|
+
];
|
|
1125
|
+
const NOISE_SESSION_PATH_PATTERNS = [/\/sodar/i, /\/recaptcha/i, /runner\.html$/i];
|
|
348
1126
|
const NETWORK_CALL_SELECT_COLUMNS = `
|
|
349
1127
|
request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class, response_size_est,
|
|
350
1128
|
request_content_type, request_body_text, request_body_json, request_body_bytes, request_body_truncated, request_body_chunk_ref,
|
|
@@ -551,84 +1329,849 @@ function resolveLastUrl(payload) {
|
|
|
551
1329
|
}
|
|
552
1330
|
return undefined;
|
|
553
1331
|
}
|
|
554
|
-
function
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
sessionId: row.session_id,
|
|
560
|
-
timestamp: row.ts,
|
|
561
|
-
type: row.type,
|
|
562
|
-
summary: describeEvent(row.type, payload),
|
|
1332
|
+
function classifySessionUrl(urlValue) {
|
|
1333
|
+
if (!urlValue) {
|
|
1334
|
+
return {
|
|
1335
|
+
kind: 'unknown',
|
|
1336
|
+
note: 'No session URL is available yet.',
|
|
563
1337
|
};
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
1338
|
+
}
|
|
1339
|
+
try {
|
|
1340
|
+
const parsed = new URL(urlValue);
|
|
1341
|
+
const host = parsed.hostname.toLowerCase();
|
|
1342
|
+
const pathname = parsed.pathname.toLowerCase();
|
|
1343
|
+
const origin = parsed.origin;
|
|
1344
|
+
const isLocalhost = host === 'localhost' || host === '127.0.0.1';
|
|
1345
|
+
if (NOISE_SESSION_HOST_PATTERNS.some((pattern) => pattern.test(host))
|
|
1346
|
+
|| NOISE_SESSION_PATH_PATTERNS.some((pattern) => pattern.test(pathname))) {
|
|
1347
|
+
return {
|
|
1348
|
+
kind: 'likely_iframe_noise',
|
|
1349
|
+
note: 'Last URL looks like third-party iframe/ad traffic rather than the app surface.',
|
|
1350
|
+
origin,
|
|
1351
|
+
host,
|
|
1352
|
+
isLocalhost,
|
|
1353
|
+
};
|
|
570
1354
|
}
|
|
571
|
-
if (
|
|
572
|
-
|
|
1355
|
+
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
|
1356
|
+
return {
|
|
1357
|
+
kind: 'top_level_page',
|
|
1358
|
+
note: isLocalhost
|
|
1359
|
+
? 'Last URL looks like a local top-level app page.'
|
|
1360
|
+
: 'Last URL looks like a top-level app page.',
|
|
1361
|
+
origin,
|
|
1362
|
+
host,
|
|
1363
|
+
isLocalhost,
|
|
1364
|
+
};
|
|
573
1365
|
}
|
|
574
|
-
|
|
1366
|
+
}
|
|
1367
|
+
catch {
|
|
1368
|
+
return {
|
|
1369
|
+
kind: 'unknown',
|
|
1370
|
+
note: 'Session URL could not be parsed.',
|
|
1371
|
+
};
|
|
575
1372
|
}
|
|
576
1373
|
return {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
timestamp: row.ts,
|
|
580
|
-
type: row.type,
|
|
581
|
-
tabId: row.tab_id ?? (typeof payload.tabId === 'number' ? payload.tabId : undefined),
|
|
582
|
-
origin: row.origin
|
|
583
|
-
?? (typeof payload.origin === 'string' ? payload.origin : undefined)
|
|
584
|
-
?? undefined,
|
|
585
|
-
payload,
|
|
1374
|
+
kind: 'unknown',
|
|
1375
|
+
note: 'Session URL does not use an http(s) page origin.',
|
|
586
1376
|
};
|
|
587
1377
|
}
|
|
588
|
-
function
|
|
589
|
-
if (
|
|
590
|
-
return
|
|
1378
|
+
function getSessionStatus(row) {
|
|
1379
|
+
if (row.ended_at) {
|
|
1380
|
+
return 'ended';
|
|
591
1381
|
}
|
|
592
|
-
if (
|
|
593
|
-
return '
|
|
1382
|
+
if (row.paused_at) {
|
|
1383
|
+
return 'paused';
|
|
594
1384
|
}
|
|
595
|
-
return '
|
|
1385
|
+
return 'active';
|
|
596
1386
|
}
|
|
597
|
-
function
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1387
|
+
function buildOverrideProfileRecords() {
|
|
1388
|
+
const summary = getOverridePocConfigSummary();
|
|
1389
|
+
return summary.profiles.map((profile) => ({
|
|
1390
|
+
profileId: profile.profileId,
|
|
1391
|
+
name: profile.name,
|
|
1392
|
+
active: profile.profileId === summary.activeProfileId,
|
|
1393
|
+
configEnabled: summary.configEnabled,
|
|
1394
|
+
enabled: profile.enabled,
|
|
1395
|
+
effectiveEnabled: summary.configEnabled && profile.enabled && profile.enabledRuleCount > 0,
|
|
1396
|
+
autoReload: profile.autoReload,
|
|
1397
|
+
configPath: summary.configPath,
|
|
1398
|
+
fileExists: profile.fileExists,
|
|
1399
|
+
ruleCount: profile.ruleCount,
|
|
1400
|
+
enabledRuleCount: profile.enabledRuleCount,
|
|
1401
|
+
rules: profile.rules,
|
|
1402
|
+
}));
|
|
1403
|
+
}
|
|
1404
|
+
function resolveOverrideProfileRecord(value) {
|
|
1405
|
+
const profiles = buildOverrideProfileRecords();
|
|
1406
|
+
const fallbackProfileId = typeof profiles[0]?.profileId === 'string' ? profiles[0].profileId : 'poc';
|
|
1407
|
+
const requestedProfileId = typeof value === 'string' && value.trim().length > 0 ? value.trim() : fallbackProfileId;
|
|
1408
|
+
const profile = profiles.find((candidate) => candidate.profileId === requestedProfileId);
|
|
1409
|
+
if (!profile) {
|
|
1410
|
+
throw new Error(`Unknown override profile: ${requestedProfileId}`);
|
|
603
1411
|
}
|
|
604
|
-
return
|
|
1412
|
+
return profile;
|
|
605
1413
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
1414
|
+
const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
1415
|
+
function sha256Text(value) {
|
|
1416
|
+
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
1417
|
+
}
|
|
1418
|
+
function isRecordWithRscFlightMetadata(value) {
|
|
1419
|
+
return isRecord(value)
|
|
1420
|
+
&& (value.productionMode === 'structured-flight-v1' && value.patchKind === 'string-value-text'
|
|
1421
|
+
|| value.productionMode === 'literal-response-v1' && value.patchKind === 'literal-text')
|
|
1422
|
+
&& value.source !== undefined
|
|
1423
|
+
&& value.patchKind !== undefined;
|
|
1424
|
+
}
|
|
1425
|
+
function buildRscFlightRuleIssues(rule) {
|
|
1426
|
+
const ruleId = String(rule.ruleId ?? 'unknown');
|
|
1427
|
+
const issues = [];
|
|
1428
|
+
const rscFlight = rule.rscFlight;
|
|
1429
|
+
if (!isRecordWithRscFlightMetadata(rscFlight)) {
|
|
1430
|
+
return [{
|
|
1431
|
+
code: 'UNSUPPORTED_RSC_FLIGHT_RULE',
|
|
1432
|
+
severity: 'error',
|
|
1433
|
+
message: `Rule ${ruleId} targets a Next.js RSC flight response without production RSC metadata generated by the response planner.`,
|
|
1434
|
+
}];
|
|
609
1435
|
}
|
|
610
|
-
const
|
|
611
|
-
if (
|
|
612
|
-
|
|
1436
|
+
const source = rscFlight.source;
|
|
1437
|
+
if (source !== 'cdp-response' && source !== 'extension-fetch') {
|
|
1438
|
+
issues.push({
|
|
1439
|
+
code: 'RSC_FLIGHT_METADATA_INVALID',
|
|
1440
|
+
severity: 'error',
|
|
1441
|
+
message: `Rule ${ruleId} RSC metadata source must be cdp-response or extension-fetch.`,
|
|
1442
|
+
});
|
|
613
1443
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1444
|
+
if (!Array.isArray(rscFlight.textPatches) || rscFlight.textPatches.length === 0) {
|
|
1445
|
+
issues.push({
|
|
1446
|
+
code: 'RSC_FLIGHT_PATCHES_INVALID',
|
|
1447
|
+
severity: 'error',
|
|
1448
|
+
message: `Rule ${ruleId} RSC flight metadata must include string-value text patches.`,
|
|
1449
|
+
});
|
|
619
1450
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1451
|
+
else {
|
|
1452
|
+
for (const [index, patch] of rscFlight.textPatches.entries()) {
|
|
1453
|
+
if (!isRecord(patch)
|
|
1454
|
+
|| typeof patch.search !== 'string'
|
|
1455
|
+
|| patch.search.length === 0
|
|
1456
|
+
|| typeof patch.replacement !== 'string'
|
|
1457
|
+
|| typeof patch.expectedCount !== 'number'
|
|
1458
|
+
|| !Number.isFinite(patch.expectedCount)
|
|
1459
|
+
|| patch.expectedCount < 0) {
|
|
1460
|
+
issues.push({
|
|
1461
|
+
code: 'RSC_FLIGHT_PATCHES_INVALID',
|
|
1462
|
+
severity: 'error',
|
|
1463
|
+
message: `Rule ${ruleId} RSC flight textPatches[${index}] is invalid.`,
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
626
1467
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1468
|
+
if (rule.requestMethod !== 'GET') {
|
|
1469
|
+
issues.push({
|
|
1470
|
+
code: 'RSC_FLIGHT_METHOD_UNSUPPORTED',
|
|
1471
|
+
severity: 'error',
|
|
1472
|
+
message: `Rule ${ruleId} RSC flight overrides only support GET requests.`,
|
|
1473
|
+
});
|
|
630
1474
|
}
|
|
631
|
-
|
|
1475
|
+
const targetAssetUrl = typeof rule.targetAssetUrl === 'string' ? rule.targetAssetUrl : '';
|
|
1476
|
+
try {
|
|
1477
|
+
const parsed = new URL(targetAssetUrl);
|
|
1478
|
+
if (!parsed.searchParams.has('_rsc')) {
|
|
1479
|
+
issues.push({
|
|
1480
|
+
code: 'RSC_FLIGHT_TARGET_INVALID',
|
|
1481
|
+
severity: 'error',
|
|
1482
|
+
message: `Rule ${ruleId} RSC flight targetAssetUrl must include the _rsc search parameter.`,
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
catch {
|
|
1487
|
+
issues.push({
|
|
1488
|
+
code: 'RSC_FLIGHT_TARGET_INVALID',
|
|
1489
|
+
severity: 'error',
|
|
1490
|
+
message: `Rule ${ruleId} RSC flight targetAssetUrl must be an absolute http(s) URL.`,
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
const contentType = typeof rule.contentType === 'string' ? rule.contentType : '';
|
|
1494
|
+
const metadataContentType = typeof rscFlight.contentType === 'string' ? rscFlight.contentType : '';
|
|
1495
|
+
if (!contentType.toLowerCase().includes('text/x-component') || !metadataContentType.toLowerCase().includes('text/x-component')) {
|
|
1496
|
+
issues.push({
|
|
1497
|
+
code: 'RSC_FLIGHT_CONTENT_TYPE_INVALID',
|
|
1498
|
+
severity: 'error',
|
|
1499
|
+
message: `Rule ${ruleId} RSC flight overrides require text/x-component content types.`,
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
const originalSha256 = typeof rscFlight.originalSha256 === 'string' ? rscFlight.originalSha256 : '';
|
|
1503
|
+
const patchedSha256 = typeof rscFlight.patchedSha256 === 'string' ? rscFlight.patchedSha256 : '';
|
|
1504
|
+
if (!SHA256_HEX_PATTERN.test(originalSha256) || !SHA256_HEX_PATTERN.test(patchedSha256) || originalSha256 === patchedSha256) {
|
|
1505
|
+
issues.push({
|
|
1506
|
+
code: 'RSC_FLIGHT_HASH_INVALID',
|
|
1507
|
+
severity: 'error',
|
|
1508
|
+
message: `Rule ${ruleId} RSC flight metadata must include distinct original and patched sha256 hashes.`,
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
const patchedBytes = typeof rscFlight.patchedBytes === 'number' && Number.isFinite(rscFlight.patchedBytes)
|
|
1512
|
+
? Math.floor(rscFlight.patchedBytes)
|
|
1513
|
+
: null;
|
|
1514
|
+
if (patchedBytes === null || patchedBytes < 1) {
|
|
1515
|
+
issues.push({
|
|
1516
|
+
code: 'RSC_FLIGHT_BYTES_INVALID',
|
|
1517
|
+
severity: 'error',
|
|
1518
|
+
message: `Rule ${ruleId} RSC flight metadata must include a positive patchedBytes value.`,
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
const fileSizeBytes = typeof rule.fileSizeBytes === 'number' && Number.isFinite(rule.fileSizeBytes)
|
|
1522
|
+
? Math.floor(rule.fileSizeBytes)
|
|
1523
|
+
: null;
|
|
1524
|
+
if (patchedBytes !== null && fileSizeBytes !== null && patchedBytes !== fileSizeBytes) {
|
|
1525
|
+
issues.push({
|
|
1526
|
+
code: 'RSC_FLIGHT_LOCAL_FILE_MISMATCH',
|
|
1527
|
+
severity: 'error',
|
|
1528
|
+
message: `Rule ${ruleId} local RSC file size does not match patchedBytes metadata.`,
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
const resolvedLocalFilePath = typeof rule.resolvedLocalFilePath === 'string' ? rule.resolvedLocalFilePath : '';
|
|
1532
|
+
if (resolvedLocalFilePath && existsSync(resolvedLocalFilePath) && SHA256_HEX_PATTERN.test(patchedSha256)) {
|
|
1533
|
+
const body = readFileSync(resolvedLocalFilePath, 'utf8');
|
|
1534
|
+
if (!/(^|\n)\d+:/u.test(body)) {
|
|
1535
|
+
issues.push({
|
|
1536
|
+
code: 'RSC_FLIGHT_BODY_INVALID',
|
|
1537
|
+
severity: 'error',
|
|
1538
|
+
message: `Rule ${ruleId} local RSC file does not match the supported Flight payload shape.`,
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
if (sha256Text(body) !== patchedSha256) {
|
|
1542
|
+
issues.push({
|
|
1543
|
+
code: 'RSC_FLIGHT_LOCAL_FILE_MISMATCH',
|
|
1544
|
+
severity: 'error',
|
|
1545
|
+
message: `Rule ${ruleId} local RSC file hash does not match patchedSha256 metadata.`,
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return issues;
|
|
1550
|
+
}
|
|
1551
|
+
function buildOverrideProfileIssues(profile) {
|
|
1552
|
+
const issues = [];
|
|
1553
|
+
const rules = Array.isArray(profile.rules)
|
|
1554
|
+
? profile.rules.filter((rule) => isRecord(rule))
|
|
1555
|
+
: [];
|
|
1556
|
+
if (profile.configEnabled !== true) {
|
|
1557
|
+
issues.push({
|
|
1558
|
+
code: 'CONFIG_DISABLED',
|
|
1559
|
+
severity: 'warning',
|
|
1560
|
+
message: 'The override config is disabled and cannot replace requests until enabled.',
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
if (profile.enabled !== true) {
|
|
1564
|
+
issues.push({
|
|
1565
|
+
code: 'PROFILE_DISABLED',
|
|
1566
|
+
severity: 'warning',
|
|
1567
|
+
message: 'The override profile is disabled and cannot replace requests until enabled.',
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
if (rules.length === 0 || !rules.some((rule) => rule.enabled === true)) {
|
|
1571
|
+
issues.push({
|
|
1572
|
+
code: 'NO_ENABLED_RULES',
|
|
1573
|
+
severity: 'error',
|
|
1574
|
+
message: 'The override profile has no enabled rules.',
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
for (const rule of rules) {
|
|
1578
|
+
if (rule.enabled !== true) {
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
if (typeof rule.targetAssetUrl !== 'string' || !rule.targetAssetUrl.startsWith('http')) {
|
|
1582
|
+
issues.push({
|
|
1583
|
+
code: 'TARGET_URL_INVALID',
|
|
1584
|
+
severity: 'error',
|
|
1585
|
+
message: `Rule ${String(rule.ruleId ?? 'unknown')} targetAssetUrl must be an absolute http(s) URL.`,
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
if (rule.fileExists !== true) {
|
|
1589
|
+
issues.push({
|
|
1590
|
+
code: 'LOCAL_FILE_MISSING',
|
|
1591
|
+
severity: 'error',
|
|
1592
|
+
message: `Rule ${String(rule.ruleId ?? 'unknown')} local override file does not exist.`,
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
issues.push(...classifyOverrideResponseRequestCapability({
|
|
1596
|
+
ruleId: rule.ruleId,
|
|
1597
|
+
requestMethod: rule.requestMethod,
|
|
1598
|
+
requestHeaders: rule.requestHeaders,
|
|
1599
|
+
ruleType: rule.ruleType,
|
|
1600
|
+
}).issues.map((issue) => ({ ...issue })));
|
|
1601
|
+
if (rule.ruleType === 'rsc-flight') {
|
|
1602
|
+
issues.push(...buildRscFlightRuleIssues(rule));
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return issues;
|
|
1606
|
+
}
|
|
1607
|
+
function buildOverrideProfileNextActions(profile, issues) {
|
|
1608
|
+
if (issues.some((issue) => issue.code === 'SERVER_ACTION_UNSUPPORTED')) {
|
|
1609
|
+
return [{
|
|
1610
|
+
code: 'REPLAN_SERVER_ACTION_OVERRIDE',
|
|
1611
|
+
message: 'Server actions stay unsupported in production override mode; replace the flow with a GET document/data/API response path instead.',
|
|
1612
|
+
}];
|
|
1613
|
+
}
|
|
1614
|
+
if (issues.some((issue) => issue.code === 'MUTATION_REPLAY_UNSUPPORTED')) {
|
|
1615
|
+
return [{
|
|
1616
|
+
code: 'REPLAN_MUTATION_OVERRIDE',
|
|
1617
|
+
message: 'Mutation responses are not replay-safe; move the override to a GET document/data/API response or remove the non-GET rule.',
|
|
1618
|
+
}];
|
|
1619
|
+
}
|
|
1620
|
+
if (issues.some((issue) => issue.code === 'UNSAFE_REQUEST_METHOD')) {
|
|
1621
|
+
return [{
|
|
1622
|
+
code: 'REPLAN_GET_ONLY_OVERRIDE',
|
|
1623
|
+
message: 'Response override rules are production-safe only for GET requests; regenerate or remove non-GET rules.',
|
|
1624
|
+
}];
|
|
1625
|
+
}
|
|
1626
|
+
if (issues.some((issue) => issue.code === 'LOCAL_FILE_MISSING')) {
|
|
1627
|
+
return [{
|
|
1628
|
+
code: 'REBUILD_OR_FIX_LOCAL_PATHS',
|
|
1629
|
+
message: 'Rebuild the local app or fix localFilePath values before enabling overrides.',
|
|
1630
|
+
}];
|
|
1631
|
+
}
|
|
1632
|
+
if (issues.some((issue) => issue.code === 'NO_ENABLED_RULES')) {
|
|
1633
|
+
return [{
|
|
1634
|
+
code: 'ENABLE_RULES',
|
|
1635
|
+
message: 'Enable at least one rule in the selected override profile.',
|
|
1636
|
+
}];
|
|
1637
|
+
}
|
|
1638
|
+
if (issues.some((issue) => issue.code === 'TARGET_URL_INVALID')) {
|
|
1639
|
+
return [{
|
|
1640
|
+
code: 'FIX_TARGET_URLS',
|
|
1641
|
+
message: 'Use absolute http(s) production URLs for every targetAssetUrl.',
|
|
1642
|
+
}];
|
|
1643
|
+
}
|
|
1644
|
+
if (issues.some((issue) => typeof issue.code === 'string' && issue.code.startsWith('RSC_FLIGHT_'))
|
|
1645
|
+
|| issues.some((issue) => issue.code === 'UNSUPPORTED_RSC_FLIGHT_RULE')) {
|
|
1646
|
+
return [{
|
|
1647
|
+
code: 'REPLAN_RSC_RESPONSE_OVERRIDE',
|
|
1648
|
+
message: 'Regenerate the RSC rule with plan_override_response_patch from a captured text/x-component response body.',
|
|
1649
|
+
}];
|
|
1650
|
+
}
|
|
1651
|
+
if (profile.configEnabled !== true) {
|
|
1652
|
+
return [{
|
|
1653
|
+
code: 'ENABLE_CONFIG',
|
|
1654
|
+
message: 'Set the root override config enabled=true after reviewing the profile.',
|
|
1655
|
+
}];
|
|
1656
|
+
}
|
|
1657
|
+
if (profile.enabled !== true) {
|
|
1658
|
+
return [{
|
|
1659
|
+
code: 'ENABLE_PROFILE',
|
|
1660
|
+
message: 'Set the selected override profile enabled=true after reviewing its rules.',
|
|
1661
|
+
}];
|
|
1662
|
+
}
|
|
1663
|
+
return [{
|
|
1664
|
+
code: 'ENABLE_OVERRIDES',
|
|
1665
|
+
message: 'Enable overrides on a connected session, then reload the target tab if needed.',
|
|
1666
|
+
}];
|
|
1667
|
+
}
|
|
1668
|
+
function hasEnabledExperimentalRscFlightRule(profile) {
|
|
1669
|
+
const rules = Array.isArray(profile.rules)
|
|
1670
|
+
? profile.rules.filter((rule) => isRecord(rule))
|
|
1671
|
+
: [];
|
|
1672
|
+
return rules.some((rule) => {
|
|
1673
|
+
return rule.enabled === true
|
|
1674
|
+
&& rule.ruleType === 'rsc-flight'
|
|
1675
|
+
&& rule.allowExperimentalRscFlightFulfillment === true;
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
function canBypassPreflightForExperimentalRsc(profile, blockingCodes) {
|
|
1679
|
+
return blockingCodes.length > 0
|
|
1680
|
+
&& blockingCodes.every((code) => code === 'UNSUPPORTED_RSC_FLIGHT_RULE')
|
|
1681
|
+
&& hasEnabledExperimentalRscFlightRule(profile);
|
|
1682
|
+
}
|
|
1683
|
+
const OVERRIDE_VARIANT_HEADER_ALLOWLIST = new Set([
|
|
1684
|
+
'accept',
|
|
1685
|
+
'content-type',
|
|
1686
|
+
'next-router-prefetch',
|
|
1687
|
+
'next-router-state-tree',
|
|
1688
|
+
'purpose',
|
|
1689
|
+
'rsc',
|
|
1690
|
+
'x-nextjs-data',
|
|
1691
|
+
]);
|
|
1692
|
+
function normalizeOverrideVariantHeaders(value) {
|
|
1693
|
+
if (!isRecord(value)) {
|
|
1694
|
+
return {};
|
|
1695
|
+
}
|
|
1696
|
+
const normalized = {};
|
|
1697
|
+
for (const [rawName, rawValue] of Object.entries(value)) {
|
|
1698
|
+
const name = rawName.trim().toLowerCase();
|
|
1699
|
+
if (!OVERRIDE_VARIANT_HEADER_ALLOWLIST.has(name)) {
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
if (typeof rawValue === 'string' && rawValue.trim().length > 0) {
|
|
1703
|
+
normalized[name] = rawValue.trim();
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
if (typeof rawValue === 'number' || typeof rawValue === 'boolean') {
|
|
1707
|
+
normalized[name] = String(rawValue);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
return normalized;
|
|
1711
|
+
}
|
|
1712
|
+
function buildOverrideVariantContext(options) {
|
|
1713
|
+
const targetUrl = normalizeOptionalString(options.targetUrl);
|
|
1714
|
+
if (!targetUrl) {
|
|
1715
|
+
return null;
|
|
1716
|
+
}
|
|
1717
|
+
const requestMethod = normalizeOverrideRequestMethod(options.requestMethod);
|
|
1718
|
+
const matchMode = normalizeOptionalString(options.matchMode) ?? 'exact';
|
|
1719
|
+
const ruleType = normalizeOptionalString(options.ruleType) ?? 'document';
|
|
1720
|
+
const captureMode = normalizeOptionalString(options.captureMode);
|
|
1721
|
+
const source = normalizeOptionalString(options.source);
|
|
1722
|
+
const headers = normalizeOverrideVariantHeaders(options.requestHeaders);
|
|
1723
|
+
const isPrefetchVariant = headers['next-router-prefetch'] === '1'
|
|
1724
|
+
|| headers.purpose?.toLowerCase() === 'prefetch';
|
|
1725
|
+
const isRscRequest = ruleType === 'rsc-flight' || headers.rsc === '1';
|
|
1726
|
+
let isNextDataRequest = ruleType === 'next-data' || headers['x-nextjs-data'] === '1';
|
|
1727
|
+
let origin;
|
|
1728
|
+
let pathname;
|
|
1729
|
+
let searchParams = [];
|
|
1730
|
+
try {
|
|
1731
|
+
const parsed = new URL(targetUrl);
|
|
1732
|
+
origin = parsed.origin;
|
|
1733
|
+
pathname = parsed.pathname;
|
|
1734
|
+
searchParams = Array.from(parsed.searchParams.entries()).map(([name, value]) => ({ name, value }));
|
|
1735
|
+
if (pathname.startsWith('/_next/data/')) {
|
|
1736
|
+
isNextDataRequest = true;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
catch {
|
|
1740
|
+
pathname = undefined;
|
|
1741
|
+
}
|
|
1742
|
+
const searchParamKeys = [...new Set(searchParams.map((entry) => entry.name))].sort();
|
|
1743
|
+
const variantBasis = {
|
|
1744
|
+
targetUrl,
|
|
1745
|
+
origin: origin ?? null,
|
|
1746
|
+
pathname: pathname ?? null,
|
|
1747
|
+
searchParams,
|
|
1748
|
+
requestMethod,
|
|
1749
|
+
matchMode,
|
|
1750
|
+
ruleType,
|
|
1751
|
+
captureMode: captureMode ?? null,
|
|
1752
|
+
source: source ?? null,
|
|
1753
|
+
triggerReload: options.triggerReload === true,
|
|
1754
|
+
headers,
|
|
1755
|
+
isPrefetchVariant,
|
|
1756
|
+
isRscRequest,
|
|
1757
|
+
isNextDataRequest,
|
|
1758
|
+
};
|
|
1759
|
+
return {
|
|
1760
|
+
...variantBasis,
|
|
1761
|
+
searchParamKeys,
|
|
1762
|
+
variantKey: sha256Text(JSON.stringify(variantBasis)),
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
function extractPlanVariantContext(plan) {
|
|
1766
|
+
if (isRecord(plan.patchSummary) && isRecord(plan.patchSummary.variantContext)) {
|
|
1767
|
+
return plan.patchSummary.variantContext;
|
|
1768
|
+
}
|
|
1769
|
+
if (isRecord(plan.capturedFromLiveSession)) {
|
|
1770
|
+
if (isRecord(plan.capturedFromLiveSession.variantContext)) {
|
|
1771
|
+
return plan.capturedFromLiveSession.variantContext;
|
|
1772
|
+
}
|
|
1773
|
+
return buildOverrideVariantContext({
|
|
1774
|
+
targetUrl: plan.capturedFromLiveSession.targetUrl ?? plan.targetAssetUrl,
|
|
1775
|
+
requestMethod: plan.capturedFromLiveSession.requestMethod ?? plan.requestMethod,
|
|
1776
|
+
matchMode: plan.capturedFromLiveSession.matchMode ?? plan.matchMode,
|
|
1777
|
+
ruleType: plan.capturedFromLiveSession.ruleType ?? plan.ruleType,
|
|
1778
|
+
captureMode: plan.capturedFromLiveSession.captureMode,
|
|
1779
|
+
source: plan.capturedFromLiveSession.source,
|
|
1780
|
+
triggerReload: plan.capturedFromLiveSession.triggerReload,
|
|
1781
|
+
requestHeaders: plan.capturedFromLiveSession.requestHeaders,
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
return buildOverrideVariantContext({
|
|
1785
|
+
targetUrl: plan.targetAssetUrl,
|
|
1786
|
+
requestMethod: plan.requestMethod,
|
|
1787
|
+
matchMode: plan.matchMode,
|
|
1788
|
+
ruleType: plan.ruleType,
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
function pushOverridePreflightIssue(issues, issue) {
|
|
1792
|
+
const code = typeof issue.code === 'string' ? issue.code : '';
|
|
1793
|
+
const source = typeof issue.source === 'string' ? issue.source : '';
|
|
1794
|
+
const message = typeof issue.message === 'string' ? issue.message : '';
|
|
1795
|
+
if (issues.some((existing) => existing.code === code && existing.source === source && existing.message === message)) {
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
issues.push(issue);
|
|
1799
|
+
}
|
|
1800
|
+
function buildOverridePreflight(options) {
|
|
1801
|
+
const session = options.db
|
|
1802
|
+
.prepare(`
|
|
1803
|
+
SELECT
|
|
1804
|
+
session_id,
|
|
1805
|
+
created_at,
|
|
1806
|
+
last_seen_at,
|
|
1807
|
+
paused_at,
|
|
1808
|
+
ended_at,
|
|
1809
|
+
tab_id,
|
|
1810
|
+
window_id,
|
|
1811
|
+
url_start,
|
|
1812
|
+
url_last,
|
|
1813
|
+
user_agent,
|
|
1814
|
+
viewport_w,
|
|
1815
|
+
viewport_h,
|
|
1816
|
+
dpr,
|
|
1817
|
+
safe_mode,
|
|
1818
|
+
pinned
|
|
1819
|
+
FROM sessions
|
|
1820
|
+
WHERE session_id = ?
|
|
1821
|
+
LIMIT 1
|
|
1822
|
+
`)
|
|
1823
|
+
.get(options.sessionId);
|
|
1824
|
+
const profile = resolveOverrideProfileRecord(options.profileId);
|
|
1825
|
+
const issues = [];
|
|
1826
|
+
const observedAssets = session
|
|
1827
|
+
? listObservedOverrideAssets(options.db, { sessionId: options.sessionId, limit: 200 })
|
|
1828
|
+
: [];
|
|
1829
|
+
const latestRun = session ? listOverridePocRuns(options.db, options.sessionId, 1, 0).runs[0] ?? null : null;
|
|
1830
|
+
const recentPlans = session
|
|
1831
|
+
? listOverridePlanAudits(options.db, { sessionId: options.sessionId, limit: 5, offset: 0 }).plans
|
|
1832
|
+
: [];
|
|
1833
|
+
const variantContexts = [...new Map(recentPlans
|
|
1834
|
+
.map((plan) => extractPlanVariantContext(plan))
|
|
1835
|
+
.filter((context) => context !== null)
|
|
1836
|
+
.map((context) => [String(context.variantKey ?? JSON.stringify(context)), context])).values()];
|
|
1837
|
+
const sessionState = options.getSessionConnectionState?.(options.sessionId);
|
|
1838
|
+
const diagnosis = session ? diagnoseOverridePoc(options.db, options.sessionId, latestRun?.runId) : null;
|
|
1839
|
+
for (const issue of buildOverrideProfileIssues(profile)) {
|
|
1840
|
+
pushOverridePreflightIssue(issues, { ...issue, source: 'profile' });
|
|
1841
|
+
}
|
|
1842
|
+
if (!session) {
|
|
1843
|
+
pushOverridePreflightIssue(issues, {
|
|
1844
|
+
code: 'SESSION_NOT_FOUND',
|
|
1845
|
+
severity: 'error',
|
|
1846
|
+
source: 'session',
|
|
1847
|
+
message: `Session not found: ${options.sessionId}`,
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
else {
|
|
1851
|
+
const sessionStatus = getSessionStatus(session);
|
|
1852
|
+
if (sessionStatus === 'paused') {
|
|
1853
|
+
pushOverridePreflightIssue(issues, {
|
|
1854
|
+
code: 'SESSION_PAUSED',
|
|
1855
|
+
severity: 'error',
|
|
1856
|
+
source: 'session',
|
|
1857
|
+
message: `Session ${options.sessionId} is paused and cannot enable overrides until it resumes.`,
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
if (sessionStatus === 'ended') {
|
|
1861
|
+
pushOverridePreflightIssue(issues, {
|
|
1862
|
+
code: 'SESSION_ENDED',
|
|
1863
|
+
severity: 'error',
|
|
1864
|
+
source: 'session',
|
|
1865
|
+
message: `Session ${options.sessionId} has ended and cannot enable overrides.`,
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
if (sessionState && sessionState.connected !== true) {
|
|
1869
|
+
pushOverridePreflightIssue(issues, {
|
|
1870
|
+
code: LIVE_SESSION_DISCONNECTED_CODE,
|
|
1871
|
+
severity: 'error',
|
|
1872
|
+
source: 'connection',
|
|
1873
|
+
message: `Session ${options.sessionId} is not currently connected to the live extension bridge.`,
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
const enabledRules = Array.isArray(profile.rules)
|
|
1878
|
+
? profile.rules.filter((rule) => isRecord(rule) && rule.enabled === true)
|
|
1879
|
+
: [];
|
|
1880
|
+
const anyServiceWorkerControlled = observedAssets.some((asset) => asset.serviceWorkerControlled);
|
|
1881
|
+
const cspMetaTags = [...new Set(observedAssets.flatMap((asset) => asset.cspMetaTags))];
|
|
1882
|
+
if (observedAssets.length === 0) {
|
|
1883
|
+
pushOverridePreflightIssue(issues, {
|
|
1884
|
+
code: 'NO_OBSERVED_ASSETS',
|
|
1885
|
+
severity: 'warning',
|
|
1886
|
+
source: 'observed-assets',
|
|
1887
|
+
message: 'No observed production assets are stored for this session yet.',
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
for (const rule of enabledRules) {
|
|
1891
|
+
const ruleId = String(rule.ruleId ?? 'unknown');
|
|
1892
|
+
const targetAssetUrl = normalizeOptionalString(rule.targetAssetUrl);
|
|
1893
|
+
if (!targetAssetUrl) {
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
const requestMethod = normalizeOverrideRequestMethod(rule.requestMethod);
|
|
1897
|
+
const matchingAssets = observedAssets.filter((asset) => {
|
|
1898
|
+
return asset.url === targetAssetUrl
|
|
1899
|
+
&& normalizeOverrideRequestMethod(asset.requestMethod) === requestMethod;
|
|
1900
|
+
});
|
|
1901
|
+
if (observedAssets.length > 0 && matchingAssets.length === 0) {
|
|
1902
|
+
pushOverridePreflightIssue(issues, {
|
|
1903
|
+
code: 'TARGET_ASSET_NOT_OBSERVED',
|
|
1904
|
+
severity: 'warning',
|
|
1905
|
+
source: 'observed-assets',
|
|
1906
|
+
message: `Rule ${ruleId} target asset was not observed for ${requestMethod} ${targetAssetUrl}.`,
|
|
1907
|
+
});
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
for (const asset of matchingAssets) {
|
|
1911
|
+
if (typeof asset.integrity === 'string' && asset.integrity.length > 0) {
|
|
1912
|
+
pushOverridePreflightIssue(issues, {
|
|
1913
|
+
code: 'TARGET_ASSET_SRI_PRESENT',
|
|
1914
|
+
severity: 'error',
|
|
1915
|
+
source: 'observed-assets',
|
|
1916
|
+
message: `Rule ${ruleId} target asset ${asset.url} includes integrity="${asset.integrity}" and cannot be overridden safely.`,
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (anyServiceWorkerControlled) {
|
|
1922
|
+
pushOverridePreflightIssue(issues, {
|
|
1923
|
+
code: 'SERVICE_WORKER_CONTROLLED',
|
|
1924
|
+
severity: 'warning',
|
|
1925
|
+
source: 'observed-assets',
|
|
1926
|
+
message: 'The observed page is service-worker controlled; verify the target requests still reach the network path that the debugger can fulfill.',
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
if (cspMetaTags.length > 0) {
|
|
1930
|
+
pushOverridePreflightIssue(issues, {
|
|
1931
|
+
code: 'CSP_META_PRESENT',
|
|
1932
|
+
severity: 'warning',
|
|
1933
|
+
source: 'observed-assets',
|
|
1934
|
+
message: `The observed page emitted ${cspMetaTags.length} CSP meta tag(s); document or bootstrap rewrites may still be constrained by page policy.`,
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
const ready = !issues.some((issue) => issue.severity === 'error');
|
|
1938
|
+
const nextActions = !ready
|
|
1939
|
+
? issues.some((issue) => issue.code === 'SERVER_ACTION_UNSUPPORTED')
|
|
1940
|
+
? [{
|
|
1941
|
+
code: 'REPLAN_SERVER_ACTION_OVERRIDE',
|
|
1942
|
+
message: 'Server actions stay unsupported in production override mode; move the override to a GET document/data/API response.',
|
|
1943
|
+
}]
|
|
1944
|
+
: issues.some((issue) => issue.code === 'MUTATION_REPLAY_UNSUPPORTED')
|
|
1945
|
+
? [{
|
|
1946
|
+
code: 'REPLAN_MUTATION_OVERRIDE',
|
|
1947
|
+
message: 'Mutation responses are not replay-safe; use a GET document/data/API response path instead.',
|
|
1948
|
+
}]
|
|
1949
|
+
: issues.some((issue) => issue.code === 'UNSAFE_REQUEST_METHOD')
|
|
1950
|
+
? [{ code: 'REPLAN_GET_ONLY_OVERRIDE', message: 'Remove or regenerate non-GET rules before enabling overrides.' }]
|
|
1951
|
+
: issues.some((issue) => issue.code === 'TARGET_ASSET_SRI_PRESENT')
|
|
1952
|
+
? [{ code: 'CHOOSE_ANOTHER_OVERRIDE_PATH', message: 'Choose a document/data response path or remove SRI on the production asset before enabling overrides.' }]
|
|
1953
|
+
: issues.some((issue) => issue.code === 'SESSION_NOT_FOUND' || issue.code === 'SESSION_PAUSED' || issue.code === 'SESSION_ENDED' || issue.code === LIVE_SESSION_DISCONNECTED_CODE)
|
|
1954
|
+
? [{ code: 'RECONNECT_SESSION', message: 'Reconnect or resume the target session before enabling overrides.' }]
|
|
1955
|
+
: buildOverrideProfileNextActions(profile, issues)
|
|
1956
|
+
: observedAssets.length === 0
|
|
1957
|
+
? [{ code: 'OBSERVE_OVERRIDE_ASSETS', message: 'Run observe_override_assets on the target route before enabling overrides in production workflows.' }]
|
|
1958
|
+
: [{ code: 'ENABLE_OVERRIDES', message: 'Preflight checks passed; the selected profile can be enabled on the live session.' }];
|
|
1959
|
+
return {
|
|
1960
|
+
ready,
|
|
1961
|
+
profileId: profile.profileId,
|
|
1962
|
+
profile,
|
|
1963
|
+
session: session
|
|
1964
|
+
? {
|
|
1965
|
+
sessionId: session.session_id,
|
|
1966
|
+
status: getSessionStatus(session),
|
|
1967
|
+
lastSeenAt: resolveSessionLastSeenAt(session, sessionState),
|
|
1968
|
+
connected: sessionState?.connected === true,
|
|
1969
|
+
disconnectedAt: sessionState?.disconnectedAt,
|
|
1970
|
+
disconnectReason: sessionState?.disconnectReason,
|
|
1971
|
+
urlLast: session.url_last ?? undefined,
|
|
1972
|
+
tabId: session.tab_id ?? undefined,
|
|
1973
|
+
}
|
|
1974
|
+
: null,
|
|
1975
|
+
issues,
|
|
1976
|
+
checks: {
|
|
1977
|
+
sessionFound: session !== undefined,
|
|
1978
|
+
connected: sessionState?.connected === true,
|
|
1979
|
+
observedAssetCount: observedAssets.length,
|
|
1980
|
+
targetAssetObserved: issues.every((issue) => issue.code !== 'TARGET_ASSET_NOT_OBSERVED'),
|
|
1981
|
+
serviceWorkerControlled: anyServiceWorkerControlled,
|
|
1982
|
+
cspMetaTagCount: cspMetaTags.length,
|
|
1983
|
+
recentPlanCount: recentPlans.length,
|
|
1984
|
+
variantContextCount: variantContexts.length,
|
|
1985
|
+
},
|
|
1986
|
+
observedAssets: {
|
|
1987
|
+
count: observedAssets.length,
|
|
1988
|
+
serviceWorkerControlled: anyServiceWorkerControlled,
|
|
1989
|
+
cspMetaTags,
|
|
1990
|
+
},
|
|
1991
|
+
latestRun,
|
|
1992
|
+
recentPlans,
|
|
1993
|
+
variantContexts,
|
|
1994
|
+
diagnosis,
|
|
1995
|
+
nextActions,
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
function normalizeOptionalBooleanInput(value, fieldName) {
|
|
1999
|
+
if (value === undefined) {
|
|
2000
|
+
return undefined;
|
|
2001
|
+
}
|
|
2002
|
+
if (typeof value !== 'boolean') {
|
|
2003
|
+
throw new Error(`${fieldName} must be a boolean when provided`);
|
|
2004
|
+
}
|
|
2005
|
+
return value;
|
|
2006
|
+
}
|
|
2007
|
+
function normalizeOptionalNumberInput(value, fieldName) {
|
|
2008
|
+
if (value === undefined) {
|
|
2009
|
+
return undefined;
|
|
2010
|
+
}
|
|
2011
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2012
|
+
throw new Error(`${fieldName} must be a finite number when provided`);
|
|
2013
|
+
}
|
|
2014
|
+
return value;
|
|
2015
|
+
}
|
|
2016
|
+
function normalizeOptionalStringArrayInput(value, fieldName) {
|
|
2017
|
+
if (value === undefined) {
|
|
2018
|
+
return undefined;
|
|
2019
|
+
}
|
|
2020
|
+
if (!Array.isArray(value)) {
|
|
2021
|
+
throw new Error(`${fieldName} must be an array of strings when provided`);
|
|
2022
|
+
}
|
|
2023
|
+
return value.map((entry, index) => {
|
|
2024
|
+
if (typeof entry !== 'string' || entry.trim().length === 0) {
|
|
2025
|
+
throw new Error(`${fieldName}[${index}] must be a non-empty string`);
|
|
2026
|
+
}
|
|
2027
|
+
return entry.trim();
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
function resolveSessionLastSeenAt(row, state) {
|
|
2031
|
+
return Math.max(row.created_at, row.last_seen_at ?? 0, row.paused_at ?? 0, row.ended_at ?? 0, state?.lastHeartbeatAt ?? 0);
|
|
2032
|
+
}
|
|
2033
|
+
function buildLiveConnectionRecord(row, scope, state) {
|
|
2034
|
+
const status = getSessionStatus(row);
|
|
2035
|
+
const lastSeenAt = resolveSessionLastSeenAt(row, state);
|
|
2036
|
+
const heartbeatAt = state?.lastHeartbeatAt;
|
|
2037
|
+
const heartbeatAgeMs = typeof heartbeatAt === 'number' ? Math.max(0, Date.now() - heartbeatAt) : undefined;
|
|
2038
|
+
const likelyStale = Boolean(!state?.connected
|
|
2039
|
+
&& status === 'active'
|
|
2040
|
+
&& scope.kind !== 'likely_iframe_noise'
|
|
2041
|
+
&& typeof heartbeatAt === 'number'
|
|
2042
|
+
&& Date.now() - heartbeatAt <= STALE_LIVE_CONNECTION_GRACE_WINDOW_MS);
|
|
2043
|
+
return {
|
|
2044
|
+
connected: state?.connected === true,
|
|
2045
|
+
connectedAt: state?.connectedAt,
|
|
2046
|
+
lastHeartbeatAt: heartbeatAt,
|
|
2047
|
+
heartbeatAgeMs,
|
|
2048
|
+
disconnectedAt: state?.disconnectedAt,
|
|
2049
|
+
disconnectReason: state?.disconnectReason ?? (status === 'ended' ? 'manual_stop' : undefined),
|
|
2050
|
+
status: status === 'ended'
|
|
2051
|
+
? 'ended'
|
|
2052
|
+
: status === 'paused'
|
|
2053
|
+
? 'paused'
|
|
2054
|
+
: state?.connected
|
|
2055
|
+
? 'connected'
|
|
2056
|
+
: likelyStale
|
|
2057
|
+
? 'likely_stale'
|
|
2058
|
+
: 'disconnected',
|
|
2059
|
+
captureReady: state?.connected === true && status === 'active',
|
|
2060
|
+
recommendedForLiveCapture: state?.connected === true && status === 'active' && scope.kind !== 'likely_iframe_noise',
|
|
2061
|
+
lastSeenAt,
|
|
2062
|
+
activityAgeMs: Math.max(0, Date.now() - lastSeenAt),
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
function buildLiveSessionNextAction(liveConnection, scope) {
|
|
2066
|
+
const liveStatus = typeof liveConnection.status === 'string' ? liveConnection.status : 'disconnected';
|
|
2067
|
+
if (liveStatus === 'connected' && scope.kind !== 'likely_iframe_noise') {
|
|
2068
|
+
return 'Use this session for live capture tools.';
|
|
2069
|
+
}
|
|
2070
|
+
if (liveStatus === 'connected' && scope.kind === 'likely_iframe_noise') {
|
|
2071
|
+
return 'Reconnect on a top-level app tab before relying on live navigation or performance captures.';
|
|
2072
|
+
}
|
|
2073
|
+
if (liveStatus === 'likely_stale') {
|
|
2074
|
+
return 'Retry list_sessions after a fresh app interaction or restart the session if live capture still fails.';
|
|
2075
|
+
}
|
|
2076
|
+
if (liveStatus === 'paused') {
|
|
2077
|
+
return 'Resume the session from the extension popup before using live capture tools.';
|
|
2078
|
+
}
|
|
2079
|
+
if (liveStatus === 'ended') {
|
|
2080
|
+
return 'Start a new extension session before using live capture tools.';
|
|
2081
|
+
}
|
|
2082
|
+
return 'Reconnect or restart the extension session before using live capture tools.';
|
|
2083
|
+
}
|
|
2084
|
+
function buildLiveSessionRecommendedAction(liveConnection, scope) {
|
|
2085
|
+
const liveStatus = typeof liveConnection.status === 'string' ? liveConnection.status : 'disconnected';
|
|
2086
|
+
if (liveStatus === 'connected' && scope.kind !== 'likely_iframe_noise') {
|
|
2087
|
+
return 'ready';
|
|
2088
|
+
}
|
|
2089
|
+
if (liveStatus === 'ended') {
|
|
2090
|
+
return 'start_new_session';
|
|
2091
|
+
}
|
|
2092
|
+
if (liveStatus === 'paused') {
|
|
2093
|
+
return 'resume_session';
|
|
2094
|
+
}
|
|
2095
|
+
return 'reconnect_extension';
|
|
2096
|
+
}
|
|
2097
|
+
function mapEventRecord(row, profile = 'legacy', options = {}) {
|
|
2098
|
+
const payload = readJsonPayload(row.payload_json);
|
|
2099
|
+
if (profile === 'compact') {
|
|
2100
|
+
const compact = {
|
|
2101
|
+
eventId: row.event_id,
|
|
2102
|
+
sessionId: row.session_id,
|
|
2103
|
+
timestamp: row.ts,
|
|
2104
|
+
type: row.type,
|
|
2105
|
+
summary: describeEvent(row.type, payload),
|
|
2106
|
+
};
|
|
2107
|
+
if (row.type === 'console') {
|
|
2108
|
+
compact.level = typeof payload.level === 'string' ? payload.level : undefined;
|
|
2109
|
+
compact.message = typeof payload.message === 'string' ? payload.message : undefined;
|
|
2110
|
+
}
|
|
2111
|
+
if (row.type === 'nav') {
|
|
2112
|
+
compact.url = resolveLastUrl(payload);
|
|
2113
|
+
}
|
|
2114
|
+
if (options.includePayload === true) {
|
|
2115
|
+
compact.payload = payload;
|
|
2116
|
+
}
|
|
2117
|
+
return compact;
|
|
2118
|
+
}
|
|
2119
|
+
return {
|
|
2120
|
+
eventId: row.event_id,
|
|
2121
|
+
sessionId: row.session_id,
|
|
2122
|
+
timestamp: row.ts,
|
|
2123
|
+
type: row.type,
|
|
2124
|
+
tabId: row.tab_id ?? (typeof payload.tabId === 'number' ? payload.tabId : undefined),
|
|
2125
|
+
origin: row.origin
|
|
2126
|
+
?? (typeof payload.origin === 'string' ? payload.origin : undefined)
|
|
2127
|
+
?? undefined,
|
|
2128
|
+
payload,
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
function classifyNetworkFailure(status, errorClass) {
|
|
2132
|
+
if (errorClass && errorClass.length > 0) {
|
|
2133
|
+
return errorClass;
|
|
2134
|
+
}
|
|
2135
|
+
if (typeof status === 'number' && status >= 400) {
|
|
2136
|
+
return 'http_error';
|
|
2137
|
+
}
|
|
2138
|
+
return 'unknown';
|
|
2139
|
+
}
|
|
2140
|
+
function buildNetworkFailureFilter(errorType) {
|
|
2141
|
+
if (typeof errorType !== 'string' || errorType.length === 0) {
|
|
2142
|
+
return '(error_class IS NOT NULL OR COALESCE(status, 0) >= 400)';
|
|
2143
|
+
}
|
|
2144
|
+
if (errorType === 'http_error') {
|
|
2145
|
+
return "(error_class = 'http_error' OR (error_class IS NULL AND COALESCE(status, 0) >= 400))";
|
|
2146
|
+
}
|
|
2147
|
+
return 'error_class = ?';
|
|
2148
|
+
}
|
|
2149
|
+
function resolveWindowSeconds(value, fallback, maxValue) {
|
|
2150
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2151
|
+
return fallback;
|
|
2152
|
+
}
|
|
2153
|
+
const floored = Math.floor(value);
|
|
2154
|
+
if (floored < 1) {
|
|
2155
|
+
return fallback;
|
|
2156
|
+
}
|
|
2157
|
+
return Math.min(floored, maxValue);
|
|
2158
|
+
}
|
|
2159
|
+
function resolveOptionalTimestamp(value) {
|
|
2160
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2161
|
+
return undefined;
|
|
2162
|
+
}
|
|
2163
|
+
const floored = Math.floor(value);
|
|
2164
|
+
return floored < 0 ? undefined : floored;
|
|
2165
|
+
}
|
|
2166
|
+
function resolveChunkBytes(value, fallback) {
|
|
2167
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2168
|
+
return fallback;
|
|
2169
|
+
}
|
|
2170
|
+
const floored = Math.floor(value);
|
|
2171
|
+
if (floored < 1) {
|
|
2172
|
+
return fallback;
|
|
2173
|
+
}
|
|
2174
|
+
return Math.min(floored, MAX_SNAPSHOT_ASSET_CHUNK_BYTES);
|
|
632
2175
|
}
|
|
633
2176
|
function resolveDurationMs(value, fallback, maxValue) {
|
|
634
2177
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
@@ -831,6 +2374,56 @@ function mapSnapshotMetadata(row) {
|
|
|
831
2374
|
createdAt: row.created_at,
|
|
832
2375
|
};
|
|
833
2376
|
}
|
|
2377
|
+
function mapAutomationRunRecord(row) {
|
|
2378
|
+
return {
|
|
2379
|
+
runId: row.run_id,
|
|
2380
|
+
sessionId: row.session_id,
|
|
2381
|
+
traceId: row.trace_id ?? undefined,
|
|
2382
|
+
action: row.action ?? undefined,
|
|
2383
|
+
tabId: row.tab_id ?? undefined,
|
|
2384
|
+
selector: row.selector ?? undefined,
|
|
2385
|
+
status: row.status,
|
|
2386
|
+
startedAt: row.started_at,
|
|
2387
|
+
completedAt: row.completed_at ?? undefined,
|
|
2388
|
+
durationMs: typeof row.completed_at === 'number'
|
|
2389
|
+
? Math.max(0, row.completed_at - row.started_at)
|
|
2390
|
+
: undefined,
|
|
2391
|
+
stopReason: row.stop_reason ?? undefined,
|
|
2392
|
+
target: parseJsonOrUndefined(row.target_summary_json),
|
|
2393
|
+
failure: parseJsonOrUndefined(row.failure_json),
|
|
2394
|
+
redaction: parseJsonOrUndefined(row.redaction_json),
|
|
2395
|
+
stepCount: row.step_count,
|
|
2396
|
+
lastStepAt: row.last_step_at ?? undefined,
|
|
2397
|
+
createdAt: row.created_at,
|
|
2398
|
+
updatedAt: row.updated_at,
|
|
2399
|
+
source: 'automation_runs',
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
function mapAutomationStepRecord(row) {
|
|
2403
|
+
return {
|
|
2404
|
+
stepId: row.step_id,
|
|
2405
|
+
runId: row.run_id,
|
|
2406
|
+
sessionId: row.session_id,
|
|
2407
|
+
stepOrder: row.step_order,
|
|
2408
|
+
traceId: row.trace_id ?? undefined,
|
|
2409
|
+
action: row.action,
|
|
2410
|
+
selector: row.selector ?? undefined,
|
|
2411
|
+
status: row.status,
|
|
2412
|
+
startedAt: row.started_at ?? undefined,
|
|
2413
|
+
finishedAt: row.finished_at ?? undefined,
|
|
2414
|
+
durationMs: row.duration_ms ?? undefined,
|
|
2415
|
+
tabId: row.tab_id ?? undefined,
|
|
2416
|
+
target: parseJsonOrUndefined(row.target_summary_json),
|
|
2417
|
+
redaction: parseJsonOrUndefined(row.redaction_json),
|
|
2418
|
+
failure: parseJsonOrUndefined(row.failure_json),
|
|
2419
|
+
inputMetadata: parseJsonOrUndefined(row.input_metadata_json),
|
|
2420
|
+
eventType: row.event_type,
|
|
2421
|
+
eventId: row.event_id ?? undefined,
|
|
2422
|
+
createdAt: row.created_at,
|
|
2423
|
+
updatedAt: row.updated_at,
|
|
2424
|
+
source: 'automation_steps',
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
834
2427
|
function formatUrlPath(url) {
|
|
835
2428
|
try {
|
|
836
2429
|
const parsed = new URL(url);
|
|
@@ -931,48 +2524,601 @@ function resolveCaptureAncestors(value, fallback) {
|
|
|
931
2524
|
}
|
|
932
2525
|
return Math.min(floored, 8);
|
|
933
2526
|
}
|
|
934
|
-
function
|
|
935
|
-
if (!
|
|
936
|
-
return
|
|
2527
|
+
function resolveStructuredMaxItems(value, fallback) {
|
|
2528
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2529
|
+
return fallback;
|
|
937
2530
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
2531
|
+
const floored = Math.floor(value);
|
|
2532
|
+
if (floored < 1) {
|
|
2533
|
+
return fallback;
|
|
2534
|
+
}
|
|
2535
|
+
return Math.min(floored, 100);
|
|
941
2536
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
2537
|
+
function resolveStructuredTextLength(value, fallback) {
|
|
2538
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2539
|
+
return fallback;
|
|
2540
|
+
}
|
|
2541
|
+
const floored = Math.floor(value);
|
|
2542
|
+
if (floored < 8) {
|
|
2543
|
+
return fallback;
|
|
2544
|
+
}
|
|
2545
|
+
return Math.min(floored, 200);
|
|
948
2546
|
}
|
|
949
|
-
function
|
|
950
|
-
if (!
|
|
951
|
-
|
|
2547
|
+
function resolveViewportDimension(value, axis) {
|
|
2548
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2549
|
+
throw new Error(`${axis} must be a finite number`);
|
|
952
2550
|
}
|
|
953
|
-
|
|
2551
|
+
const floored = Math.floor(value);
|
|
2552
|
+
const min = axis === 'width' ? 320 : 200;
|
|
2553
|
+
const max = axis === 'width' ? 5120 : 4320;
|
|
2554
|
+
if (floored < min || floored > max) {
|
|
2555
|
+
throw new Error(`${axis} must be between ${min} and ${max}`);
|
|
2556
|
+
}
|
|
2557
|
+
return floored;
|
|
954
2558
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
2559
|
+
class WorkflowTargetResolutionError extends Error {
|
|
2560
|
+
code;
|
|
2561
|
+
details;
|
|
2562
|
+
constructor(code, message, details) {
|
|
2563
|
+
super(message);
|
|
2564
|
+
this.name = 'WorkflowTargetResolutionError';
|
|
2565
|
+
this.code = code;
|
|
2566
|
+
this.details = details;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
function resolveOptionalMatcherString(value) {
|
|
2570
|
+
if (typeof value !== 'string') {
|
|
2571
|
+
return undefined;
|
|
2572
|
+
}
|
|
2573
|
+
const normalized = value.trim();
|
|
2574
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
2575
|
+
}
|
|
2576
|
+
function resolveOptionalMatcherBoolean(value) {
|
|
2577
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
2578
|
+
}
|
|
2579
|
+
function resolveOptionalMatcherCount(value, field) {
|
|
2580
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2581
|
+
return undefined;
|
|
2582
|
+
}
|
|
2583
|
+
const floored = Math.floor(value);
|
|
2584
|
+
if (floored < 0) {
|
|
2585
|
+
throw new Error(`${field} must be greater than or equal to 0`);
|
|
2586
|
+
}
|
|
2587
|
+
return floored;
|
|
2588
|
+
}
|
|
2589
|
+
function resolvePageStateScope(value) {
|
|
2590
|
+
if (value === 'buttons' || value === 'inputs' || value === 'modals' || value === 'focused' || value === 'page') {
|
|
2591
|
+
return value;
|
|
2592
|
+
}
|
|
2593
|
+
throw new Error('scope must be one of buttons, inputs, modals, focused, or page');
|
|
2594
|
+
}
|
|
2595
|
+
function resolvePageStateMatcher(input) {
|
|
2596
|
+
const matcher = {
|
|
2597
|
+
scope: resolvePageStateScope(input.scope),
|
|
2598
|
+
selector: resolveOptionalMatcherString(input.selector),
|
|
2599
|
+
testId: resolveOptionalMatcherString(input.testId),
|
|
2600
|
+
textContains: resolveOptionalMatcherString(input.textContains),
|
|
2601
|
+
labelContains: resolveOptionalMatcherString(input.labelContains),
|
|
2602
|
+
titleContains: resolveOptionalMatcherString(input.titleContains),
|
|
2603
|
+
urlContains: resolveOptionalMatcherString(input.urlContains),
|
|
2604
|
+
language: resolveOptionalMatcherString(input.language),
|
|
2605
|
+
disabled: resolveOptionalMatcherBoolean(input.disabled),
|
|
2606
|
+
selected: resolveOptionalMatcherBoolean(input.selected),
|
|
2607
|
+
pressed: resolveOptionalMatcherBoolean(input.pressed),
|
|
2608
|
+
expanded: resolveOptionalMatcherBoolean(input.expanded),
|
|
2609
|
+
readOnly: resolveOptionalMatcherBoolean(input.readOnly),
|
|
2610
|
+
requiredField: resolveOptionalMatcherBoolean(input.requiredField),
|
|
2611
|
+
tagName: resolveOptionalMatcherString(input.tagName)?.toLowerCase(),
|
|
2612
|
+
type: resolveOptionalMatcherString(input.type)?.toLowerCase(),
|
|
2613
|
+
countExactly: resolveOptionalMatcherCount(input.countExactly, 'countExactly'),
|
|
2614
|
+
countAtLeast: resolveOptionalMatcherCount(input.countAtLeast, 'countAtLeast'),
|
|
2615
|
+
};
|
|
2616
|
+
if (matcher.countExactly !== undefined && matcher.countAtLeast !== undefined) {
|
|
2617
|
+
throw new Error('countExactly and countAtLeast cannot both be set');
|
|
2618
|
+
}
|
|
2619
|
+
return matcher;
|
|
2620
|
+
}
|
|
2621
|
+
function includesNormalized(value, needle) {
|
|
2622
|
+
if (!needle) {
|
|
2623
|
+
return true;
|
|
2624
|
+
}
|
|
2625
|
+
return typeof value === 'string' && value.toLowerCase().includes(needle.toLowerCase());
|
|
2626
|
+
}
|
|
2627
|
+
function equalsNormalized(value, expected) {
|
|
2628
|
+
if (!expected) {
|
|
2629
|
+
return true;
|
|
2630
|
+
}
|
|
2631
|
+
return typeof value === 'string' && value.toLowerCase() === expected.toLowerCase();
|
|
2632
|
+
}
|
|
2633
|
+
function equalsOptionalBoolean(value, expected) {
|
|
2634
|
+
if (expected === undefined) {
|
|
2635
|
+
return true;
|
|
2636
|
+
}
|
|
2637
|
+
return value === expected;
|
|
2638
|
+
}
|
|
2639
|
+
function pickPageStateScopeItems(payload, scope) {
|
|
2640
|
+
if (scope === 'buttons' || scope === 'inputs' || scope === 'modals') {
|
|
2641
|
+
const value = payload[scope];
|
|
2642
|
+
return asRecordArray(value);
|
|
2643
|
+
}
|
|
2644
|
+
if (scope === 'focused') {
|
|
2645
|
+
const focused = payload.focused;
|
|
2646
|
+
return typeof focused === 'object' && focused !== null ? [focused] : [];
|
|
2647
|
+
}
|
|
2648
|
+
return [payload];
|
|
2649
|
+
}
|
|
2650
|
+
function matchesPageStateItem(item, matcher) {
|
|
2651
|
+
return (includesNormalized(item.selector, matcher.selector)
|
|
2652
|
+
&& equalsNormalized(item.testId, matcher.testId)
|
|
2653
|
+
&& includesNormalized(item.text, matcher.textContains)
|
|
2654
|
+
&& includesNormalized(item.label, matcher.labelContains)
|
|
2655
|
+
&& includesNormalized(item.title, matcher.titleContains)
|
|
2656
|
+
&& includesNormalized(item.url, matcher.urlContains)
|
|
2657
|
+
&& equalsNormalized(item.language, matcher.language)
|
|
2658
|
+
&& equalsNormalized(item.tagName, matcher.tagName)
|
|
2659
|
+
&& equalsNormalized(item.type, matcher.type)
|
|
2660
|
+
&& equalsOptionalBoolean(item.disabled, matcher.disabled)
|
|
2661
|
+
&& equalsOptionalBoolean(item.selected, matcher.selected)
|
|
2662
|
+
&& equalsOptionalBoolean(item.pressed, matcher.pressed)
|
|
2663
|
+
&& equalsOptionalBoolean(item.expanded, matcher.expanded)
|
|
2664
|
+
&& equalsOptionalBoolean(item.readOnly, matcher.readOnly)
|
|
2665
|
+
&& equalsOptionalBoolean(item.required, matcher.requiredField));
|
|
2666
|
+
}
|
|
2667
|
+
function evaluatePageStateAssertion(payload, matcher) {
|
|
2668
|
+
const scopeItems = pickPageStateScopeItems(payload, matcher.scope);
|
|
2669
|
+
const matchingItems = scopeItems.filter((item) => matchesPageStateItem(item, matcher));
|
|
2670
|
+
const matchCount = matchingItems.length;
|
|
2671
|
+
const matched = matcher.countExactly !== undefined
|
|
2672
|
+
? matchCount === matcher.countExactly
|
|
2673
|
+
: matcher.countAtLeast !== undefined
|
|
2674
|
+
? matchCount >= matcher.countAtLeast
|
|
2675
|
+
: matchCount >= 1;
|
|
2676
|
+
return {
|
|
2677
|
+
matched,
|
|
2678
|
+
matchCount,
|
|
2679
|
+
sampledMatches: matchingItems.slice(0, 5),
|
|
2680
|
+
expectedCount: {
|
|
2681
|
+
countExactly: matcher.countExactly,
|
|
2682
|
+
countAtLeast: matcher.countAtLeast,
|
|
2683
|
+
},
|
|
2684
|
+
summary: typeof payload.summary === 'object' && payload.summary !== null
|
|
2685
|
+
? payload.summary
|
|
2686
|
+
: undefined,
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
function extractPageSummarySnapshot(capture) {
|
|
2690
|
+
if (!capture) {
|
|
2691
|
+
return undefined;
|
|
2692
|
+
}
|
|
2693
|
+
const summary = typeof capture.payload.summary === 'object' && capture.payload.summary !== null
|
|
2694
|
+
? capture.payload.summary
|
|
2695
|
+
: undefined;
|
|
2696
|
+
const focused = typeof capture.payload.focused === 'object' && capture.payload.focused !== null
|
|
2697
|
+
? capture.payload.focused
|
|
2698
|
+
: undefined;
|
|
2699
|
+
return {
|
|
2700
|
+
url: typeof capture.payload.url === 'string' ? capture.payload.url : undefined,
|
|
2701
|
+
language: typeof capture.payload.language === 'string' ? capture.payload.language : undefined,
|
|
2702
|
+
summary,
|
|
2703
|
+
focusedText: typeof focused?.text === 'string' ? focused.text : undefined,
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
function createPageChangeSummary(previousCapture, currentCapture) {
|
|
2707
|
+
const previous = extractPageSummarySnapshot(previousCapture);
|
|
2708
|
+
const current = extractPageSummarySnapshot(currentCapture);
|
|
2709
|
+
if (!current) {
|
|
2710
|
+
return undefined;
|
|
2711
|
+
}
|
|
2712
|
+
const changes = [];
|
|
2713
|
+
const previousSummary = previous?.summary;
|
|
2714
|
+
const currentSummary = current.summary;
|
|
2715
|
+
const summaryDelta = {};
|
|
2716
|
+
for (const key of ['buttons', 'inputs', 'modals']) {
|
|
2717
|
+
const previousValue = typeof previousSummary?.[key] === 'number' ? previousSummary[key] : undefined;
|
|
2718
|
+
const currentValue = typeof currentSummary?.[key] === 'number' ? currentSummary[key] : undefined;
|
|
2719
|
+
if (previousValue !== currentValue && currentValue !== undefined) {
|
|
2720
|
+
summaryDelta[key] = {
|
|
2721
|
+
previous: previousValue,
|
|
2722
|
+
current: currentValue,
|
|
2723
|
+
};
|
|
2724
|
+
changes.push(`${key} ${previousValue ?? 0} -> ${currentValue}`);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
if (previous?.url && current.url && previous.url !== current.url) {
|
|
2728
|
+
changes.push(`url changed`);
|
|
2729
|
+
}
|
|
2730
|
+
if (previous?.language && current.language && previous.language !== current.language) {
|
|
2731
|
+
changes.push(`language ${previous.language} -> ${current.language}`);
|
|
2732
|
+
}
|
|
2733
|
+
if ((previous?.focusedText ?? '') !== (current.focusedText ?? '') && current.focusedText) {
|
|
2734
|
+
changes.push('focused element changed');
|
|
2735
|
+
}
|
|
2736
|
+
return {
|
|
2737
|
+
changes,
|
|
2738
|
+
previous: previous ?? null,
|
|
2739
|
+
current,
|
|
2740
|
+
summaryDelta,
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
function resolveInteractiveKinds(value) {
|
|
2744
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
2745
|
+
return ['buttons', 'inputs', 'modals', 'focused'];
|
|
2746
|
+
}
|
|
2747
|
+
const allowed = new Set(['buttons', 'inputs', 'modals', 'focused']);
|
|
2748
|
+
const kinds = value
|
|
2749
|
+
.filter((entry) => typeof entry === 'string' && allowed.has(entry))
|
|
2750
|
+
.map((entry) => entry);
|
|
2751
|
+
return kinds.length > 0 ? Array.from(new Set(kinds)) : ['buttons', 'inputs', 'modals', 'focused'];
|
|
2752
|
+
}
|
|
2753
|
+
function collectInteractiveElementRefs(payload, kinds, maxItems) {
|
|
2754
|
+
const refs = [];
|
|
2755
|
+
for (const kind of kinds) {
|
|
2756
|
+
if (kind === 'focused') {
|
|
2757
|
+
const focused = typeof payload.focused === 'object' && payload.focused !== null
|
|
2758
|
+
? payload.focused
|
|
2759
|
+
: undefined;
|
|
2760
|
+
if (focused?.elementRef) {
|
|
2761
|
+
refs.push({
|
|
2762
|
+
kind,
|
|
2763
|
+
...focused,
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
2768
|
+
for (const item of asRecordArray(payload[kind])) {
|
|
2769
|
+
refs.push({
|
|
2770
|
+
kind,
|
|
2771
|
+
...item,
|
|
2772
|
+
});
|
|
2773
|
+
if (refs.length >= maxItems) {
|
|
2774
|
+
return refs.slice(0, maxItems);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
return refs.slice(0, maxItems);
|
|
2779
|
+
}
|
|
2780
|
+
async function waitForPageStateConditionDetailed(sessionId, input, capturePageState, initialCapture) {
|
|
2781
|
+
const matcher = resolvePageStateMatcher(input);
|
|
2782
|
+
const timeoutMs = resolveTimeoutMs(input.timeoutMs, 5_000, 30_000);
|
|
2783
|
+
const pollIntervalMs = resolveDurationMs(input.pollIntervalMs, 50, 2_000) ?? 200;
|
|
2784
|
+
const startedAt = Date.now();
|
|
2785
|
+
const deadline = startedAt + timeoutMs;
|
|
2786
|
+
let attempts = 0;
|
|
2787
|
+
let lastCapture = initialCapture;
|
|
2788
|
+
let lastAssertion;
|
|
2789
|
+
if (lastCapture) {
|
|
2790
|
+
lastAssertion = evaluatePageStateAssertion(lastCapture.payload, matcher);
|
|
2791
|
+
if (lastAssertion.matched) {
|
|
2792
|
+
return {
|
|
2793
|
+
limitsApplied: lastCapture.limitsApplied,
|
|
2794
|
+
matcher,
|
|
2795
|
+
matched: true,
|
|
2796
|
+
matchCount: lastAssertion.matchCount,
|
|
2797
|
+
expectedCount: lastAssertion.expectedCount,
|
|
2798
|
+
sampledMatches: lastAssertion.sampledMatches,
|
|
2799
|
+
pageSummary: lastAssertion.summary,
|
|
2800
|
+
page: {
|
|
2801
|
+
url: lastCapture.payload.url,
|
|
2802
|
+
title: lastCapture.payload.title,
|
|
2803
|
+
language: lastCapture.payload.language,
|
|
2804
|
+
viewport: lastCapture.payload.viewport,
|
|
2805
|
+
},
|
|
2806
|
+
waitedMs: 0,
|
|
2807
|
+
attempts,
|
|
2808
|
+
pollIntervalMs,
|
|
2809
|
+
lastCapture,
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
while (Date.now() <= deadline) {
|
|
2814
|
+
attempts += 1;
|
|
2815
|
+
lastCapture = await capturePageState(sessionId, input);
|
|
2816
|
+
lastAssertion = evaluatePageStateAssertion(lastCapture.payload, matcher);
|
|
2817
|
+
if (lastAssertion.matched) {
|
|
2818
|
+
return {
|
|
2819
|
+
limitsApplied: lastCapture.limitsApplied,
|
|
2820
|
+
matcher,
|
|
2821
|
+
matched: true,
|
|
2822
|
+
matchCount: lastAssertion.matchCount,
|
|
2823
|
+
expectedCount: lastAssertion.expectedCount,
|
|
2824
|
+
sampledMatches: lastAssertion.sampledMatches,
|
|
2825
|
+
pageSummary: lastAssertion.summary,
|
|
2826
|
+
page: {
|
|
2827
|
+
url: lastCapture.payload.url,
|
|
2828
|
+
title: lastCapture.payload.title,
|
|
2829
|
+
language: lastCapture.payload.language,
|
|
2830
|
+
viewport: lastCapture.payload.viewport,
|
|
2831
|
+
},
|
|
2832
|
+
waitedMs: Date.now() - startedAt,
|
|
2833
|
+
attempts,
|
|
2834
|
+
pollIntervalMs,
|
|
2835
|
+
lastCapture,
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
await sleep(pollIntervalMs);
|
|
2839
|
+
}
|
|
2840
|
+
return {
|
|
2841
|
+
limitsApplied: lastCapture?.limitsApplied ?? { maxResults: 0, truncated: false },
|
|
2842
|
+
matcher,
|
|
2843
|
+
matched: false,
|
|
2844
|
+
matchCount: lastAssertion?.matchCount ?? 0,
|
|
2845
|
+
expectedCount: lastAssertion?.expectedCount ?? {
|
|
2846
|
+
countExactly: matcher.countExactly,
|
|
2847
|
+
countAtLeast: matcher.countAtLeast,
|
|
2848
|
+
},
|
|
2849
|
+
sampledMatches: lastAssertion?.sampledMatches ?? [],
|
|
2850
|
+
pageSummary: lastAssertion?.summary,
|
|
2851
|
+
page: lastCapture
|
|
2852
|
+
? {
|
|
2853
|
+
url: lastCapture.payload.url,
|
|
2854
|
+
title: lastCapture.payload.title,
|
|
2855
|
+
language: lastCapture.payload.language,
|
|
2856
|
+
viewport: lastCapture.payload.viewport,
|
|
2857
|
+
}
|
|
2858
|
+
: undefined,
|
|
2859
|
+
waitedMs: Date.now() - startedAt,
|
|
2860
|
+
attempts,
|
|
2861
|
+
pollIntervalMs,
|
|
2862
|
+
timeoutMs,
|
|
2863
|
+
lastCapture,
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
async function waitForPageStateCondition(sessionId, input, capturePageState) {
|
|
2867
|
+
const detailed = await waitForPageStateConditionDetailed(sessionId, input, capturePageState);
|
|
2868
|
+
const { lastCapture: _lastCapture, ...waited } = detailed;
|
|
2869
|
+
return waited;
|
|
2870
|
+
}
|
|
2871
|
+
function candidateTextForWorkflowTarget(item) {
|
|
2872
|
+
return [item.text, item.label, item.title]
|
|
2873
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
2874
|
+
.join(' ')
|
|
2875
|
+
.trim();
|
|
2876
|
+
}
|
|
2877
|
+
function describeWorkflowTargetCandidate(item) {
|
|
2878
|
+
return {
|
|
2879
|
+
text: candidateTextForWorkflowTarget(item) || undefined,
|
|
2880
|
+
testId: typeof item.testId === 'string' ? item.testId : undefined,
|
|
2881
|
+
selector: typeof item.selector === 'string' ? item.selector : undefined,
|
|
2882
|
+
tagName: typeof item.tagName === 'string' ? item.tagName : undefined,
|
|
2883
|
+
type: typeof item.type === 'string' ? item.type : undefined,
|
|
2884
|
+
disabled: typeof item.disabled === 'boolean' ? item.disabled : undefined,
|
|
2885
|
+
selected: typeof item.selected === 'boolean' ? item.selected : undefined,
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
function pickWorkflowTargetItems(payload, scope) {
|
|
2889
|
+
if (scope) {
|
|
2890
|
+
return pickPageStateScopeItems(payload, scope);
|
|
2891
|
+
}
|
|
2892
|
+
return [
|
|
2893
|
+
...pickPageStateScopeItems(payload, 'buttons'),
|
|
2894
|
+
...pickPageStateScopeItems(payload, 'inputs'),
|
|
2895
|
+
...pickPageStateScopeItems(payload, 'modals'),
|
|
2896
|
+
...pickPageStateScopeItems(payload, 'focused'),
|
|
2897
|
+
];
|
|
2898
|
+
}
|
|
2899
|
+
function matchesWorkflowActionTarget(item, target) {
|
|
2900
|
+
return (equalsNormalized(item.testId, target.testId)
|
|
2901
|
+
&& includesNormalized(item.text, target.textContains)
|
|
2902
|
+
&& includesNormalized(item.label, target.labelContains)
|
|
2903
|
+
&& includesNormalized(item.title, target.titleContains)
|
|
2904
|
+
&& equalsNormalized(item.tagName, target.tagName)
|
|
2905
|
+
&& equalsNormalized(item.type, target.type)
|
|
2906
|
+
&& equalsOptionalBoolean(item.disabled, target.disabled)
|
|
2907
|
+
&& equalsOptionalBoolean(item.selected, target.selected)
|
|
2908
|
+
&& equalsOptionalBoolean(item.pressed, target.pressed)
|
|
2909
|
+
&& equalsOptionalBoolean(item.expanded, target.expanded)
|
|
2910
|
+
&& equalsOptionalBoolean(item.readOnly, target.readOnly)
|
|
2911
|
+
&& equalsOptionalBoolean(item.required, target.requiredField)
|
|
2912
|
+
&& (typeof item.elementRef === 'string' || typeof item.selector === 'string'));
|
|
2913
|
+
}
|
|
2914
|
+
function summarizeWorkflowTargetMatcher(target) {
|
|
2915
|
+
return {
|
|
2916
|
+
scope: target.scope,
|
|
2917
|
+
selector: target.selector,
|
|
2918
|
+
elementRef: target.elementRef,
|
|
2919
|
+
testId: target.testId,
|
|
2920
|
+
textContains: target.textContains,
|
|
2921
|
+
labelContains: target.labelContains,
|
|
2922
|
+
titleContains: target.titleContains,
|
|
2923
|
+
tagName: target.tagName,
|
|
2924
|
+
type: target.type,
|
|
2925
|
+
disabled: target.disabled,
|
|
2926
|
+
selected: target.selected,
|
|
2927
|
+
pressed: target.pressed,
|
|
2928
|
+
expanded: target.expanded,
|
|
2929
|
+
readOnly: target.readOnly,
|
|
2930
|
+
requiredField: target.requiredField,
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
async function resolveWorkflowActionTarget(sessionId, target, capturePageState, existingCapture) {
|
|
2934
|
+
if (!target) {
|
|
2935
|
+
return {
|
|
2936
|
+
resolution: {
|
|
2937
|
+
strategy: 'none',
|
|
2938
|
+
},
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
if (target.elementRef || target.selector) {
|
|
2942
|
+
return {
|
|
2943
|
+
target: {
|
|
2944
|
+
elementRef: target.elementRef,
|
|
2945
|
+
selector: target.selector,
|
|
2946
|
+
tabId: target.tabId,
|
|
2947
|
+
frameId: target.frameId,
|
|
2948
|
+
url: target.url,
|
|
2949
|
+
},
|
|
2950
|
+
resolution: {
|
|
2951
|
+
strategy: target.elementRef ? 'elementRef' : 'selector',
|
|
2952
|
+
matcher: summarizeWorkflowTargetMatcher(target),
|
|
2953
|
+
},
|
|
2954
|
+
};
|
|
2955
|
+
}
|
|
2956
|
+
const capture = existingCapture ?? await capturePageState(sessionId, {
|
|
2957
|
+
includeButtons: target.scope ? target.scope === 'buttons' : true,
|
|
2958
|
+
includeInputs: target.scope ? target.scope === 'inputs' : true,
|
|
2959
|
+
includeModals: target.scope ? target.scope === 'modals' : true,
|
|
2960
|
+
maxItems: 100,
|
|
2961
|
+
maxTextLength: 120,
|
|
2962
|
+
});
|
|
2963
|
+
const candidates = pickWorkflowTargetItems(capture.payload, target.scope)
|
|
2964
|
+
.filter((item) => matchesWorkflowActionTarget(item, target));
|
|
2965
|
+
if (candidates.length === 0) {
|
|
2966
|
+
throw new WorkflowTargetResolutionError('workflow_target_not_found', 'No interactive element matched the workflow target.', {
|
|
2967
|
+
matcher: summarizeWorkflowTargetMatcher(target),
|
|
2968
|
+
searchedScope: target.scope ?? 'all-interactive',
|
|
2969
|
+
sampledCandidates: pickWorkflowTargetItems(capture.payload, target.scope)
|
|
2970
|
+
.slice(0, 5)
|
|
2971
|
+
.map((item) => describeWorkflowTargetCandidate(item)),
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
if (candidates.length > 1) {
|
|
2975
|
+
throw new WorkflowTargetResolutionError('workflow_target_ambiguous', `Workflow target matched ${candidates.length} elements; refine the matcher.`, {
|
|
2976
|
+
matcher: summarizeWorkflowTargetMatcher(target),
|
|
2977
|
+
matchedCandidateCount: candidates.length,
|
|
2978
|
+
sampledCandidates: candidates.slice(0, 5).map((item) => describeWorkflowTargetCandidate(item)),
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
const candidate = candidates[0];
|
|
2982
|
+
return {
|
|
2983
|
+
target: {
|
|
2984
|
+
elementRef: typeof candidate.elementRef === 'string' ? candidate.elementRef : undefined,
|
|
2985
|
+
selector: typeof candidate.selector === 'string' ? candidate.selector : undefined,
|
|
2986
|
+
tabId: target.tabId,
|
|
2987
|
+
frameId: target.frameId,
|
|
2988
|
+
url: target.url,
|
|
2989
|
+
},
|
|
2990
|
+
resolution: {
|
|
2991
|
+
strategy: typeof candidate.elementRef === 'string' ? 'semantic_elementRef' : 'semantic_selector',
|
|
2992
|
+
matcher: summarizeWorkflowTargetMatcher(target),
|
|
2993
|
+
matchedCandidateCount: candidates.length,
|
|
2994
|
+
matched: describeWorkflowTargetCandidate(candidate),
|
|
2995
|
+
},
|
|
2996
|
+
pageCapture: capture,
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
function createWorkflowStepId(step, index) {
|
|
3000
|
+
return step.id ?? `step_${index + 1}`;
|
|
3001
|
+
}
|
|
3002
|
+
async function captureWorkflowPageState(sessionId, capturePageState, mode) {
|
|
3003
|
+
const maxItems = mode === 'fast' ? 12 : 20;
|
|
3004
|
+
const maxTextLength = mode === 'fast' ? 60 : 80;
|
|
3005
|
+
return capturePageState(sessionId, {
|
|
3006
|
+
includeButtons: true,
|
|
3007
|
+
includeInputs: true,
|
|
3008
|
+
includeModals: true,
|
|
3009
|
+
maxItems,
|
|
3010
|
+
maxTextLength,
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
function normalizeWorkflowError(error) {
|
|
3014
|
+
if (error instanceof WorkflowTargetResolutionError) {
|
|
3015
|
+
return {
|
|
3016
|
+
code: error.code,
|
|
3017
|
+
message: `${error.message} ${JSON.stringify(error.details)}`,
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
3020
|
+
if (error instanceof z.ZodError) {
|
|
3021
|
+
return {
|
|
3022
|
+
code: 'invalid_workflow_step',
|
|
3023
|
+
message: error.issues.map((issue) => issue.message).join('; '),
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
if (error instanceof Error) {
|
|
3027
|
+
return {
|
|
3028
|
+
code: 'workflow_step_failed',
|
|
3029
|
+
message: error.message,
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
return {
|
|
3033
|
+
code: 'workflow_step_failed',
|
|
3034
|
+
message: 'Unknown workflow step failure',
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
function resolveWorkflowRecommendedAction(error) {
|
|
3038
|
+
if (!error) {
|
|
3039
|
+
return undefined;
|
|
3040
|
+
}
|
|
3041
|
+
if (error.code === LIVE_SESSION_DISCONNECTED_CODE
|
|
3042
|
+
|| error.message.includes(LIVE_SESSION_DISCONNECTED_CODE)
|
|
3043
|
+
|| error.message.toLowerCase().includes('transport closed')) {
|
|
3044
|
+
return 'reconnect_session';
|
|
3045
|
+
}
|
|
3046
|
+
if (error.code === 'target_not_found') {
|
|
3047
|
+
return 'inspect_page_state';
|
|
3048
|
+
}
|
|
3049
|
+
if (error.code === 'click_intercepted') {
|
|
3050
|
+
return 'retry_step';
|
|
3051
|
+
}
|
|
3052
|
+
if (error.code === 'workflow_target_ambiguous') {
|
|
3053
|
+
return 'refine_target';
|
|
3054
|
+
}
|
|
3055
|
+
if (error.code === 'workflow_target_not_found') {
|
|
3056
|
+
return 'inspect_page_state';
|
|
3057
|
+
}
|
|
3058
|
+
if (error.code === 'page_state_not_matched' || error.code === 'page_state_assertion_failed') {
|
|
3059
|
+
return 'inspect_page_state';
|
|
3060
|
+
}
|
|
3061
|
+
return undefined;
|
|
3062
|
+
}
|
|
3063
|
+
function resolveWorkflowFailureSelector(step, stepResultTarget) {
|
|
3064
|
+
if (step.kind === 'action') {
|
|
3065
|
+
if (typeof step.target?.selector === 'string' && step.target.selector.trim().length > 0) {
|
|
3066
|
+
return step.target.selector.trim();
|
|
3067
|
+
}
|
|
3068
|
+
const actionTarget = isRecord(stepResultTarget?.actionTarget) ? stepResultTarget?.actionTarget : undefined;
|
|
3069
|
+
if (typeof actionTarget?.selector === 'string' && actionTarget.selector.trim().length > 0) {
|
|
3070
|
+
return actionTarget.selector.trim();
|
|
3071
|
+
}
|
|
3072
|
+
const resolution = isRecord(stepResultTarget?.resolution) ? stepResultTarget?.resolution : undefined;
|
|
3073
|
+
const matched = isRecord(resolution?.matched) ? resolution.matched : undefined;
|
|
3074
|
+
if (typeof matched?.selector === 'string' && matched.selector.trim().length > 0) {
|
|
3075
|
+
return matched.selector.trim();
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
return undefined;
|
|
3079
|
+
}
|
|
3080
|
+
function asStringArray(value, maxItems) {
|
|
3081
|
+
if (!Array.isArray(value)) {
|
|
3082
|
+
return [];
|
|
3083
|
+
}
|
|
3084
|
+
return value
|
|
3085
|
+
.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
3086
|
+
.slice(0, maxItems);
|
|
3087
|
+
}
|
|
3088
|
+
const LIVE_CONSOLE_LEVELS = new Set(['log', 'info', 'warn', 'error', 'debug', 'trace']);
|
|
3089
|
+
function resolveLiveConsoleLevels(value) {
|
|
3090
|
+
const levels = asStringArray(value, 16)
|
|
3091
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
3092
|
+
.filter((entry) => LIVE_CONSOLE_LEVELS.has(entry));
|
|
3093
|
+
return Array.from(new Set(levels));
|
|
3094
|
+
}
|
|
3095
|
+
function asRecordArray(value) {
|
|
3096
|
+
if (!Array.isArray(value)) {
|
|
3097
|
+
return [];
|
|
3098
|
+
}
|
|
3099
|
+
return value.filter((entry) => typeof entry === 'object' && entry !== null);
|
|
3100
|
+
}
|
|
3101
|
+
function mapLiveConsoleLogRecord(log, profile, options = {}) {
|
|
3102
|
+
if (profile === 'compact') {
|
|
3103
|
+
const compact = {
|
|
3104
|
+
timestamp: typeof log.timestamp === 'number'
|
|
3105
|
+
? log.timestamp
|
|
3106
|
+
: typeof log.ts === 'number'
|
|
3107
|
+
? log.ts
|
|
3108
|
+
: undefined,
|
|
3109
|
+
level: typeof log.level === 'string' ? log.level : undefined,
|
|
3110
|
+
message: typeof log.message === 'string' ? log.message : '',
|
|
3111
|
+
};
|
|
3112
|
+
if (typeof log.count === 'number') {
|
|
3113
|
+
compact.count = log.count;
|
|
3114
|
+
}
|
|
3115
|
+
if (typeof log.firstTimestamp === 'number') {
|
|
3116
|
+
compact.firstTimestamp = log.firstTimestamp;
|
|
3117
|
+
}
|
|
3118
|
+
if (typeof log.lastTimestamp === 'number') {
|
|
3119
|
+
compact.lastTimestamp = log.lastTimestamp;
|
|
3120
|
+
}
|
|
3121
|
+
if (options.includeArgs === true && Array.isArray(log.args)) {
|
|
976
3122
|
compact.args = log.args;
|
|
977
3123
|
}
|
|
978
3124
|
return compact;
|
|
@@ -1027,6 +3173,243 @@ function ensureCaptureSuccess(result, sessionId) {
|
|
|
1027
3173
|
}
|
|
1028
3174
|
return result.payload ?? {};
|
|
1029
3175
|
}
|
|
3176
|
+
function auditSessionExists(db, sessionId) {
|
|
3177
|
+
const row = db.prepare('SELECT 1 FROM sessions WHERE session_id = ? LIMIT 1').get(sessionId);
|
|
3178
|
+
return row !== undefined;
|
|
3179
|
+
}
|
|
3180
|
+
function hashLocalFileIfPresent(filePath) {
|
|
3181
|
+
if (!filePath || !existsSync(filePath)) {
|
|
3182
|
+
return { sha256: null, bytes: null };
|
|
3183
|
+
}
|
|
3184
|
+
const stat = statSync(filePath);
|
|
3185
|
+
if (!stat.isFile()) {
|
|
3186
|
+
return { sha256: null, bytes: null };
|
|
3187
|
+
}
|
|
3188
|
+
return {
|
|
3189
|
+
sha256: createHash('sha256').update(readFileSync(filePath)).digest('hex'),
|
|
3190
|
+
bytes: stat.size,
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
function resolveAuditProfileId(input) {
|
|
3194
|
+
return normalizeOptionalString(input.profileId) ?? null;
|
|
3195
|
+
}
|
|
3196
|
+
function buildOverrideRollbackMetadata(options) {
|
|
3197
|
+
return {
|
|
3198
|
+
disableTool: 'disable_overrides',
|
|
3199
|
+
validateTool: 'validate_override_profile',
|
|
3200
|
+
sessionId: options.sessionId,
|
|
3201
|
+
profileId: options.profileId,
|
|
3202
|
+
configPath: options.configPath ?? null,
|
|
3203
|
+
generatedFiles: Array.from(new Set(options.generatedFiles.filter((entry) => entry.trim().length > 0))),
|
|
3204
|
+
generatedDirectories: Array.from(new Set((options.generatedDirectories ?? []).filter((entry) => entry.trim().length > 0))),
|
|
3205
|
+
notes: [
|
|
3206
|
+
'Disable overrides for this session before deleting generated files or config entries.',
|
|
3207
|
+
'Re-run validate_override_profile after editing or removing generated config rules.',
|
|
3208
|
+
options.note,
|
|
3209
|
+
].filter((entry) => typeof entry === 'string' && entry.length > 0),
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
function persistResponsePlanAudit(options) {
|
|
3213
|
+
if (!options.sessionId || !options.plan.rule || !auditSessionExists(options.db, options.sessionId)) {
|
|
3214
|
+
return undefined;
|
|
3215
|
+
}
|
|
3216
|
+
const profileId = resolveAuditProfileId(options.input);
|
|
3217
|
+
const record = {
|
|
3218
|
+
planId: randomUUID(),
|
|
3219
|
+
sessionId: options.sessionId,
|
|
3220
|
+
createdAt: Date.now(),
|
|
3221
|
+
plannerKind: 'response-patch',
|
|
3222
|
+
toolName: 'plan_override_response_patch',
|
|
3223
|
+
profileId,
|
|
3224
|
+
ruleId: options.plan.rule.ruleId,
|
|
3225
|
+
ruleType: options.plan.rule.ruleType,
|
|
3226
|
+
requestMethod: options.plan.requestMethod,
|
|
3227
|
+
matchMode: options.plan.matchMode,
|
|
3228
|
+
targetAssetUrl: options.plan.targetUrl,
|
|
3229
|
+
localFilePath: options.plan.localFilePath ?? options.plan.rule.localFilePath,
|
|
3230
|
+
configPath: options.plan.configPath ?? null,
|
|
3231
|
+
contentType: options.plan.contentType,
|
|
3232
|
+
originalSha256: options.plan.originalSha256,
|
|
3233
|
+
patchedSha256: options.plan.patchedSha256,
|
|
3234
|
+
originalBytes: options.plan.originalBytes,
|
|
3235
|
+
patchedBytes: options.plan.patchedBytes,
|
|
3236
|
+
patchSummary: {
|
|
3237
|
+
textPatches: options.plan.patches,
|
|
3238
|
+
jsonPatches: options.plan.jsonPatches,
|
|
3239
|
+
documentPatches: options.plan.documentPatches,
|
|
3240
|
+
ruleType: options.plan.ruleType,
|
|
3241
|
+
configWritten: options.plan.configWritten,
|
|
3242
|
+
rscFlight: options.plan.rule.rscFlight ?? null,
|
|
3243
|
+
variantContext: options.variantContext ?? null,
|
|
3244
|
+
},
|
|
3245
|
+
preview: options.plan.preview ?? null,
|
|
3246
|
+
warnings: options.plan.warnings,
|
|
3247
|
+
blockers: options.plan.blockers,
|
|
3248
|
+
capturedFromLiveSession: options.capturedFromLiveSession ?? null,
|
|
3249
|
+
rollback: buildOverrideRollbackMetadata({
|
|
3250
|
+
sessionId: options.sessionId,
|
|
3251
|
+
profileId,
|
|
3252
|
+
configPath: options.plan.configPath ?? null,
|
|
3253
|
+
generatedFiles: options.plan.localFilePath ? [options.plan.localFilePath] : [],
|
|
3254
|
+
note: 'Generated response override bodies are disposable once the override has been disabled.',
|
|
3255
|
+
}),
|
|
3256
|
+
};
|
|
3257
|
+
return insertOverridePlanAudit(options.db, record);
|
|
3258
|
+
}
|
|
3259
|
+
function persistNextSourcePlanAudits(options) {
|
|
3260
|
+
if (!options.sessionId || !auditSessionExists(options.db, options.sessionId)) {
|
|
3261
|
+
return [];
|
|
3262
|
+
}
|
|
3263
|
+
const sessionId = options.sessionId;
|
|
3264
|
+
const profileId = resolveAuditProfileId(options.input);
|
|
3265
|
+
const generatedFiles = options.plan.rules.map((rule) => rule.localFilePath);
|
|
3266
|
+
return options.plan.rules.map((rule) => {
|
|
3267
|
+
const localFile = hashLocalFileIfPresent(rule.localFilePath);
|
|
3268
|
+
const record = {
|
|
3269
|
+
planId: randomUUID(),
|
|
3270
|
+
sessionId: options.sessionId,
|
|
3271
|
+
createdAt: Date.now(),
|
|
3272
|
+
plannerKind: 'next-source-overlay',
|
|
3273
|
+
toolName: 'plan_next_source_override',
|
|
3274
|
+
profileId,
|
|
3275
|
+
ruleId: rule.ruleId,
|
|
3276
|
+
ruleType: rule.ruleType,
|
|
3277
|
+
requestMethod: rule.requestMethod,
|
|
3278
|
+
matchMode: rule.matchMode,
|
|
3279
|
+
targetAssetUrl: rule.targetAssetUrl,
|
|
3280
|
+
localFilePath: rule.localFilePath,
|
|
3281
|
+
configPath: options.plan.configPath ?? null,
|
|
3282
|
+
contentType: rule.contentType,
|
|
3283
|
+
originalSha256: null,
|
|
3284
|
+
patchedSha256: localFile.sha256,
|
|
3285
|
+
originalBytes: null,
|
|
3286
|
+
patchedBytes: localFile.bytes,
|
|
3287
|
+
patchSummary: {
|
|
3288
|
+
sourcePaths: options.plan.sourcePaths,
|
|
3289
|
+
editsApplied: options.plan.editsApplied,
|
|
3290
|
+
ruleReason: rule.reason,
|
|
3291
|
+
confidence: rule.confidence,
|
|
3292
|
+
score: rule.score,
|
|
3293
|
+
matchedSourcePaths: rule.matchedSourcePaths,
|
|
3294
|
+
originalAssetPath: rule.originalAssetPath ?? null,
|
|
3295
|
+
build: options.plan.build,
|
|
3296
|
+
configWritten: options.plan.configWritten,
|
|
3297
|
+
},
|
|
3298
|
+
preview: null,
|
|
3299
|
+
warnings: [...options.plan.warnings, ...rule.blockers.map((blocker) => `rule ${rule.ruleId}: ${blocker}`)],
|
|
3300
|
+
blockers: options.plan.blockers,
|
|
3301
|
+
capturedFromLiveSession: null,
|
|
3302
|
+
rollback: buildOverrideRollbackMetadata({
|
|
3303
|
+
sessionId,
|
|
3304
|
+
profileId,
|
|
3305
|
+
configPath: options.plan.configPath ?? null,
|
|
3306
|
+
generatedFiles,
|
|
3307
|
+
generatedDirectories: [options.plan.overlayRoot],
|
|
3308
|
+
note: 'Generated Next.js overlay folders are disposable once the override has been disabled.',
|
|
3309
|
+
}),
|
|
3310
|
+
};
|
|
3311
|
+
return insertOverridePlanAudit(options.db, record);
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
function normalizeSnapshotResponsePayload(payload, options) {
|
|
3315
|
+
const snapshotRecord = structuredClone(payload);
|
|
3316
|
+
const snapshotRoot = snapshotRecord.snapshot;
|
|
3317
|
+
if (typeof snapshotRoot === 'object' && snapshotRoot !== null) {
|
|
3318
|
+
const snapshotObject = snapshotRoot;
|
|
3319
|
+
if (!options.includeDom) {
|
|
3320
|
+
delete snapshotObject.dom;
|
|
3321
|
+
}
|
|
3322
|
+
if (!options.includeStyles) {
|
|
3323
|
+
delete snapshotObject.styles;
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
const png = snapshotRecord.png;
|
|
3327
|
+
if (!options.includePngDataUrl && typeof png === 'object' && png !== null) {
|
|
3328
|
+
delete png.dataUrl;
|
|
3329
|
+
}
|
|
3330
|
+
return snapshotRecord;
|
|
3331
|
+
}
|
|
3332
|
+
function resolveFailureEvidenceCaptureOptions(input) {
|
|
3333
|
+
const raw = isRecord(input.captureOnFailure) ? input.captureOnFailure : undefined;
|
|
3334
|
+
const enabled = raw !== undefined ? raw.enabled !== false : false;
|
|
3335
|
+
const mode = raw?.mode === 'png' || raw?.mode === 'both' || raw?.mode === 'dom' ? raw.mode : 'dom';
|
|
3336
|
+
const styleMode = raw?.styleMode === 'computed-full' || raw?.styleMode === 'computed-lite'
|
|
3337
|
+
? raw.styleMode
|
|
3338
|
+
: 'computed-lite';
|
|
3339
|
+
return {
|
|
3340
|
+
enabled,
|
|
3341
|
+
selector: typeof raw?.selector === 'string' && raw.selector.trim().length > 0 ? raw.selector.trim() : undefined,
|
|
3342
|
+
mode,
|
|
3343
|
+
styleMode,
|
|
3344
|
+
explicitStyleMode: raw?.styleMode === 'computed-full' || raw?.styleMode === 'computed-lite',
|
|
3345
|
+
maxDepth: resolveCaptureDepth(raw?.maxDepth, 3),
|
|
3346
|
+
maxBytes: resolveCaptureBytes(raw?.maxBytes, 50_000),
|
|
3347
|
+
maxAncestors: resolveCaptureAncestors(raw?.maxAncestors, 4),
|
|
3348
|
+
includeDom: typeof raw?.includeDom === 'boolean' ? raw.includeDom : mode !== 'png',
|
|
3349
|
+
includeStyles: typeof raw?.includeStyles === 'boolean' ? raw.includeStyles : mode !== 'png',
|
|
3350
|
+
includePngDataUrl: typeof raw?.includePngDataUrl === 'boolean' ? raw.includePngDataUrl : mode !== 'png',
|
|
3351
|
+
};
|
|
3352
|
+
}
|
|
3353
|
+
function resolveWorkflowFailurePolicy(step, stopOnFailure) {
|
|
3354
|
+
const raw = isRecord(step.onFailure) ? step.onFailure : undefined;
|
|
3355
|
+
const strategy = raw?.strategy === 'continue' || raw?.strategy === 'retry_once' || raw?.strategy === 'stop'
|
|
3356
|
+
? raw.strategy
|
|
3357
|
+
: stopOnFailure === false
|
|
3358
|
+
? 'continue'
|
|
3359
|
+
: 'stop';
|
|
3360
|
+
const captureRaw = raw && isRecord(raw.capture)
|
|
3361
|
+
? {
|
|
3362
|
+
captureOnFailure: {
|
|
3363
|
+
...raw.capture,
|
|
3364
|
+
enabled: raw.capture.enabled ?? true,
|
|
3365
|
+
},
|
|
3366
|
+
}
|
|
3367
|
+
: undefined;
|
|
3368
|
+
return {
|
|
3369
|
+
strategy,
|
|
3370
|
+
captureOptions: captureRaw ? resolveFailureEvidenceCaptureOptions(captureRaw) : undefined,
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
async function captureFailureSnapshot(captureClient, sessionId, selector, options) {
|
|
3374
|
+
if (!options.enabled) {
|
|
3375
|
+
return undefined;
|
|
3376
|
+
}
|
|
3377
|
+
try {
|
|
3378
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_UI_SNAPSHOT', {
|
|
3379
|
+
selector: options.selector ?? selector,
|
|
3380
|
+
trigger: 'error',
|
|
3381
|
+
mode: options.mode,
|
|
3382
|
+
styleMode: options.styleMode,
|
|
3383
|
+
explicitStyleMode: options.explicitStyleMode,
|
|
3384
|
+
maxDepth: options.maxDepth,
|
|
3385
|
+
maxBytes: options.maxBytes,
|
|
3386
|
+
maxAncestors: options.maxAncestors,
|
|
3387
|
+
includeDom: options.includeDom,
|
|
3388
|
+
includeStyles: options.includeStyles,
|
|
3389
|
+
includePngDataUrl: options.includePngDataUrl,
|
|
3390
|
+
llmRequested: true,
|
|
3391
|
+
}, 5_000);
|
|
3392
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
3393
|
+
return {
|
|
3394
|
+
captured: true,
|
|
3395
|
+
limitsApplied: {
|
|
3396
|
+
maxBytes: options.maxBytes,
|
|
3397
|
+
truncated: capture.truncated ?? false,
|
|
3398
|
+
},
|
|
3399
|
+
snapshot: normalizeSnapshotResponsePayload(payload, options),
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
catch (error) {
|
|
3403
|
+
const normalized = normalizeCaptureError(sessionId, error);
|
|
3404
|
+
return {
|
|
3405
|
+
captured: false,
|
|
3406
|
+
error: normalized.message,
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
async function captureFailureEvidence(captureClient, sessionId, request, options) {
|
|
3411
|
+
return captureFailureSnapshot(captureClient, sessionId, request.target?.selector, options);
|
|
3412
|
+
}
|
|
1030
3413
|
export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
1031
3414
|
return {
|
|
1032
3415
|
list_sessions: async (input) => {
|
|
@@ -1038,7 +3421,12 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1038
3421
|
const where = [];
|
|
1039
3422
|
const params = [];
|
|
1040
3423
|
if (sinceMinutes !== undefined && Number.isFinite(sinceMinutes) && sinceMinutes > 0) {
|
|
1041
|
-
where.push(
|
|
3424
|
+
where.push(`
|
|
3425
|
+
CASE
|
|
3426
|
+
WHEN COALESCE(last_seen_at, 0) > created_at THEN COALESCE(last_seen_at, 0)
|
|
3427
|
+
ELSE created_at
|
|
3428
|
+
END >= ?
|
|
3429
|
+
`);
|
|
1042
3430
|
params.push(Date.now() - Math.floor(sinceMinutes * 60_000));
|
|
1043
3431
|
}
|
|
1044
3432
|
const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
|
@@ -1046,6 +3434,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1046
3434
|
SELECT
|
|
1047
3435
|
session_id,
|
|
1048
3436
|
created_at,
|
|
3437
|
+
last_seen_at,
|
|
1049
3438
|
paused_at,
|
|
1050
3439
|
ended_at,
|
|
1051
3440
|
tab_id,
|
|
@@ -1060,49 +3449,48 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1060
3449
|
pinned
|
|
1061
3450
|
FROM sessions
|
|
1062
3451
|
${whereClause}
|
|
1063
|
-
ORDER BY
|
|
3452
|
+
ORDER BY
|
|
3453
|
+
CASE
|
|
3454
|
+
WHEN COALESCE(last_seen_at, 0) > created_at THEN COALESCE(last_seen_at, 0)
|
|
3455
|
+
ELSE created_at
|
|
3456
|
+
END DESC,
|
|
3457
|
+
created_at DESC
|
|
1064
3458
|
LIMIT ? OFFSET ?
|
|
1065
3459
|
`;
|
|
1066
3460
|
const rows = db.prepare(sql).all(...params, limit + 1, offset);
|
|
1067
3461
|
const truncatedByLimit = rows.length > limit;
|
|
1068
|
-
const sessions = rows.slice(0, limit).map((row) =>
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
:
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
lastHeartbeatAt: state.lastHeartbeatAt,
|
|
1101
|
-
disconnectedAt: state.disconnectedAt,
|
|
1102
|
-
disconnectReason: state.disconnectReason,
|
|
1103
|
-
};
|
|
1104
|
-
})(),
|
|
1105
|
-
}));
|
|
3462
|
+
const sessions = rows.slice(0, limit).map((row) => {
|
|
3463
|
+
const status = getSessionStatus(row);
|
|
3464
|
+
const state = getSessionConnectionState?.(row.session_id);
|
|
3465
|
+
const lastUrl = row.url_last ?? undefined;
|
|
3466
|
+
const scope = classifySessionUrl(lastUrl);
|
|
3467
|
+
const liveConnection = buildLiveConnectionRecord(row, scope, state);
|
|
3468
|
+
return {
|
|
3469
|
+
sessionId: row.session_id,
|
|
3470
|
+
createdAt: row.created_at,
|
|
3471
|
+
lastSeenAt: resolveSessionLastSeenAt(row, state),
|
|
3472
|
+
pausedAt: row.paused_at ?? undefined,
|
|
3473
|
+
endedAt: row.ended_at ?? undefined,
|
|
3474
|
+
status,
|
|
3475
|
+
tabId: row.tab_id ?? undefined,
|
|
3476
|
+
windowId: row.window_id ?? undefined,
|
|
3477
|
+
urlStart: row.url_start ?? undefined,
|
|
3478
|
+
urlLast: lastUrl,
|
|
3479
|
+
lastUrl,
|
|
3480
|
+
userAgent: row.user_agent ?? undefined,
|
|
3481
|
+
viewport: row.viewport_w !== null && row.viewport_h !== null
|
|
3482
|
+
? {
|
|
3483
|
+
width: row.viewport_w,
|
|
3484
|
+
height: row.viewport_h,
|
|
3485
|
+
}
|
|
3486
|
+
: undefined,
|
|
3487
|
+
dpr: row.dpr ?? undefined,
|
|
3488
|
+
safeMode: row.safe_mode === 1,
|
|
3489
|
+
pinned: row.pinned === 1,
|
|
3490
|
+
scope,
|
|
3491
|
+
liveConnection,
|
|
3492
|
+
};
|
|
3493
|
+
});
|
|
1106
3494
|
const bytePage = applyByteBudget(sessions, maxResponseBytes);
|
|
1107
3495
|
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
1108
3496
|
return {
|
|
@@ -1178,35 +3566,510 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1178
3566
|
pinned: session.pinned === 1,
|
|
1179
3567
|
};
|
|
1180
3568
|
},
|
|
1181
|
-
|
|
3569
|
+
get_live_session_health: async (input) => {
|
|
1182
3570
|
const db = getDb();
|
|
1183
3571
|
const sessionId = getSessionId(input);
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
1187
|
-
const offset = resolveOffset(input.offset);
|
|
1188
|
-
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
1189
|
-
const responseProfile = resolveResponseProfile(input.responseProfile);
|
|
1190
|
-
const includePayload = responseProfile === 'compact' && input.includePayload === true;
|
|
1191
|
-
const requestedTypes = parseRequestedTypes(input.types ?? input.eventTypes);
|
|
1192
|
-
const params = [];
|
|
1193
|
-
const where = [];
|
|
1194
|
-
if (sessionId) {
|
|
1195
|
-
where.push('session_id = ?');
|
|
1196
|
-
params.push(sessionId);
|
|
1197
|
-
}
|
|
1198
|
-
appendEventOriginFilter(where, params, origin);
|
|
1199
|
-
if (requestedTypes.length > 0) {
|
|
1200
|
-
const placeholders = requestedTypes.map(() => '?').join(', ');
|
|
1201
|
-
where.push(`type IN (${placeholders})`);
|
|
1202
|
-
params.push(...requestedTypes);
|
|
3572
|
+
if (!sessionId) {
|
|
3573
|
+
throw new Error('sessionId is required');
|
|
1203
3574
|
}
|
|
1204
|
-
const
|
|
3575
|
+
const session = db
|
|
1205
3576
|
.prepare(`
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
3577
|
+
SELECT
|
|
3578
|
+
session_id,
|
|
3579
|
+
created_at,
|
|
3580
|
+
last_seen_at,
|
|
3581
|
+
paused_at,
|
|
3582
|
+
ended_at,
|
|
3583
|
+
tab_id,
|
|
3584
|
+
window_id,
|
|
3585
|
+
url_start,
|
|
3586
|
+
url_last,
|
|
3587
|
+
viewport_w,
|
|
3588
|
+
viewport_h,
|
|
3589
|
+
dpr,
|
|
3590
|
+
safe_mode,
|
|
3591
|
+
pinned
|
|
3592
|
+
FROM sessions
|
|
3593
|
+
WHERE session_id = ?
|
|
3594
|
+
`)
|
|
3595
|
+
.get(sessionId);
|
|
3596
|
+
if (!session) {
|
|
3597
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
3598
|
+
}
|
|
3599
|
+
const latestNav = db
|
|
3600
|
+
.prepare(`
|
|
3601
|
+
SELECT payload_json
|
|
3602
|
+
FROM events
|
|
3603
|
+
WHERE session_id = ? AND type = 'nav'
|
|
3604
|
+
ORDER BY ts DESC
|
|
3605
|
+
LIMIT 1
|
|
3606
|
+
`)
|
|
3607
|
+
.get(sessionId);
|
|
3608
|
+
const navPayload = latestNav ? readJsonPayload(latestNav.payload_json) : {};
|
|
3609
|
+
const lastUrl = resolveLastUrl(navPayload) ?? session.url_last ?? undefined;
|
|
3610
|
+
const scope = classifySessionUrl(lastUrl);
|
|
3611
|
+
const connectionState = getSessionConnectionState?.(sessionId);
|
|
3612
|
+
const liveConnection = buildLiveConnectionRecord(session, scope, connectionState);
|
|
3613
|
+
const status = getSessionStatus(session);
|
|
3614
|
+
const lastSeenAt = resolveSessionLastSeenAt(session, connectionState);
|
|
3615
|
+
const nextAction = buildLiveSessionNextAction(liveConnection, scope);
|
|
3616
|
+
const recommendedAction = buildLiveSessionRecommendedAction(liveConnection, scope);
|
|
3617
|
+
const sessionRecord = {
|
|
3618
|
+
sessionId: session.session_id,
|
|
3619
|
+
createdAt: session.created_at,
|
|
3620
|
+
lastSeenAt,
|
|
3621
|
+
pausedAt: session.paused_at ?? undefined,
|
|
3622
|
+
endedAt: session.ended_at ?? undefined,
|
|
3623
|
+
status,
|
|
3624
|
+
tabId: session.tab_id ?? undefined,
|
|
3625
|
+
windowId: session.window_id ?? undefined,
|
|
3626
|
+
urlStart: session.url_start ?? undefined,
|
|
3627
|
+
urlLast: session.url_last ?? undefined,
|
|
3628
|
+
lastUrl,
|
|
3629
|
+
viewport: session.viewport_w !== null && session.viewport_h !== null
|
|
3630
|
+
? {
|
|
3631
|
+
width: session.viewport_w,
|
|
3632
|
+
height: session.viewport_h,
|
|
3633
|
+
}
|
|
3634
|
+
: undefined,
|
|
3635
|
+
dpr: session.dpr ?? undefined,
|
|
3636
|
+
safeMode: session.safe_mode === 1,
|
|
3637
|
+
pinned: session.pinned === 1,
|
|
3638
|
+
};
|
|
3639
|
+
return {
|
|
3640
|
+
...createBaseResponse(sessionId),
|
|
3641
|
+
status,
|
|
3642
|
+
createdAt: session.created_at,
|
|
3643
|
+
lastSeenAt,
|
|
3644
|
+
pausedAt: session.paused_at ?? undefined,
|
|
3645
|
+
endedAt: session.ended_at ?? undefined,
|
|
3646
|
+
tabId: session.tab_id ?? undefined,
|
|
3647
|
+
windowId: session.window_id ?? undefined,
|
|
3648
|
+
lastUrl,
|
|
3649
|
+
safeMode: session.safe_mode === 1,
|
|
3650
|
+
pinned: session.pinned === 1,
|
|
3651
|
+
session: sessionRecord,
|
|
3652
|
+
scope,
|
|
3653
|
+
liveConnection,
|
|
3654
|
+
nextAction,
|
|
3655
|
+
recommendedAction,
|
|
3656
|
+
};
|
|
3657
|
+
},
|
|
3658
|
+
list_override_profiles: async () => {
|
|
3659
|
+
const profiles = buildOverrideProfileRecords();
|
|
3660
|
+
return {
|
|
3661
|
+
...createBaseResponse(),
|
|
3662
|
+
limitsApplied: {
|
|
3663
|
+
maxResults: profiles.length,
|
|
3664
|
+
truncated: false,
|
|
3665
|
+
},
|
|
3666
|
+
profiles,
|
|
3667
|
+
nextActions: profiles.length > 0
|
|
3668
|
+
? [{ code: 'VALIDATE_PROFILE', message: 'Run validate_override_profile before enabling overrides.' }]
|
|
3669
|
+
: [{ code: 'CREATE_PROFILE', message: 'Run create_override_profile to generate a candidate profile.' }],
|
|
3670
|
+
};
|
|
3671
|
+
},
|
|
3672
|
+
create_override_profile: async (input) => {
|
|
3673
|
+
const adapterInput = normalizeOptionalString(input.adapter) ?? normalizeOptionalString(input.mode);
|
|
3674
|
+
let adapter;
|
|
3675
|
+
if (adapterInput !== undefined) {
|
|
3676
|
+
if (!OVERRIDE_PROFILE_ADAPTERS.includes(adapterInput)) {
|
|
3677
|
+
throw new Error(`adapter must be one of: ${OVERRIDE_PROFILE_ADAPTERS.join(', ')}`);
|
|
3678
|
+
}
|
|
3679
|
+
adapter = adapterInput;
|
|
3680
|
+
}
|
|
3681
|
+
const targetBaseUrl = normalizeOptionalString(input.targetBaseUrl);
|
|
3682
|
+
if (!targetBaseUrl) {
|
|
3683
|
+
throw new Error('targetBaseUrl is required, for example https://example.com/_next/ or https://example.com/assets/');
|
|
3684
|
+
}
|
|
3685
|
+
const generated = createOverrideProfileConfig({
|
|
3686
|
+
adapter,
|
|
3687
|
+
targetBaseUrl,
|
|
3688
|
+
projectRoot: normalizeOptionalString(input.projectRoot),
|
|
3689
|
+
assetRoot: normalizeOptionalString(input.assetRoot),
|
|
3690
|
+
nextDir: normalizeOptionalString(input.nextDir),
|
|
3691
|
+
configPath: normalizeOptionalString(input.configPath),
|
|
3692
|
+
profileId: normalizeOptionalString(input.profileId),
|
|
3693
|
+
profileName: normalizeOptionalString(input.profileName),
|
|
3694
|
+
enabled: normalizeOptionalBooleanInput(input.enabled, 'enabled'),
|
|
3695
|
+
profileEnabled: normalizeOptionalBooleanInput(input.profileEnabled, 'profileEnabled'),
|
|
3696
|
+
autoReload: normalizeOptionalBooleanInput(input.autoReload, 'autoReload'),
|
|
3697
|
+
includeManifestFiles: normalizeOptionalBooleanInput(input.includeManifestFiles, 'includeManifestFiles'),
|
|
3698
|
+
includeStaticFiles: normalizeOptionalBooleanInput(input.includeStaticFiles, 'includeStaticFiles'),
|
|
3699
|
+
extensions: normalizeOptionalStringArrayInput(input.extensions, 'extensions'),
|
|
3700
|
+
maxRules: normalizeOptionalNumberInput(input.maxRules, 'maxRules'),
|
|
3701
|
+
});
|
|
3702
|
+
const writeConfig = normalizeOptionalBooleanInput(input.writeConfig, 'writeConfig') ?? false;
|
|
3703
|
+
const overwrite = normalizeOptionalBooleanInput(input.overwrite, 'overwrite') ?? false;
|
|
3704
|
+
const write = {
|
|
3705
|
+
written: false,
|
|
3706
|
+
path: generated.suggestedConfigPath,
|
|
3707
|
+
};
|
|
3708
|
+
let nextActions = generated.nextActions;
|
|
3709
|
+
if (writeConfig && generated.ruleCount === 0) {
|
|
3710
|
+
write.failureCode = 'NO_RULES';
|
|
3711
|
+
write.message = 'Generated profile has no rules; config was not written.';
|
|
3712
|
+
nextActions = [{
|
|
3713
|
+
code: 'BUILD_APP',
|
|
3714
|
+
message: 'Build the app so local assets exist, then generate the profile again.',
|
|
3715
|
+
}];
|
|
3716
|
+
}
|
|
3717
|
+
else if (writeConfig && existsSync(generated.suggestedConfigPath) && !overwrite) {
|
|
3718
|
+
write.failureCode = 'CONFIG_EXISTS';
|
|
3719
|
+
write.message = 'Config file already exists; pass overwrite=true or choose another configPath.';
|
|
3720
|
+
nextActions = [{
|
|
3721
|
+
code: 'OVERWRITE_OR_CHOOSE_CONFIG_PATH',
|
|
3722
|
+
message: 'Pass overwrite=true to replace the config file, or choose a different configPath.',
|
|
3723
|
+
}, ...generated.nextActions];
|
|
3724
|
+
}
|
|
3725
|
+
else if (writeConfig) {
|
|
3726
|
+
mkdirSync(dirname(generated.suggestedConfigPath), { recursive: true });
|
|
3727
|
+
writeFileSync(generated.suggestedConfigPath, generated.configJson, 'utf8');
|
|
3728
|
+
write.written = true;
|
|
3729
|
+
write.bytes = Buffer.byteLength(generated.configJson, 'utf8');
|
|
3730
|
+
nextActions = generated.nextActions.filter((action) => action.code !== 'SAVE_LOCAL_CONFIG');
|
|
3731
|
+
}
|
|
3732
|
+
return {
|
|
3733
|
+
...createBaseResponse(),
|
|
3734
|
+
limitsApplied: {
|
|
3735
|
+
maxResults: generated.ruleCount,
|
|
3736
|
+
truncated: generated.warnings.some((warning) => warning.startsWith('Rule generation was limited')),
|
|
3737
|
+
},
|
|
3738
|
+
adapter: generated.adapter,
|
|
3739
|
+
mode: generated.mode,
|
|
3740
|
+
projectRoot: generated.projectRoot,
|
|
3741
|
+
assetRoot: generated.assetRoot,
|
|
3742
|
+
nextDir: generated.nextDir,
|
|
3743
|
+
targetBaseUrl: generated.targetBaseUrl,
|
|
3744
|
+
suggestedConfigPath: generated.suggestedConfigPath,
|
|
3745
|
+
ruleCount: generated.ruleCount,
|
|
3746
|
+
manifestFiles: generated.manifestFiles,
|
|
3747
|
+
staticFileCount: generated.staticFileCount,
|
|
3748
|
+
missingManifestAssetCount: generated.missingManifestAssetCount,
|
|
3749
|
+
warnings: generated.warnings,
|
|
3750
|
+
nextActions,
|
|
3751
|
+
write,
|
|
3752
|
+
profile: generated.profile,
|
|
3753
|
+
config: generated.config,
|
|
3754
|
+
configJson: generated.configJson,
|
|
3755
|
+
};
|
|
3756
|
+
},
|
|
3757
|
+
validate_override_profile: async (input) => {
|
|
3758
|
+
const profile = resolveOverrideProfileRecord(input.profileId);
|
|
3759
|
+
const issues = buildOverrideProfileIssues(profile);
|
|
3760
|
+
return {
|
|
3761
|
+
...createBaseResponse(),
|
|
3762
|
+
profileId: profile.profileId,
|
|
3763
|
+
valid: !issues.some((issue) => issue.severity === 'error'),
|
|
3764
|
+
issues,
|
|
3765
|
+
nextActions: buildOverrideProfileNextActions(profile, issues),
|
|
3766
|
+
profile,
|
|
3767
|
+
};
|
|
3768
|
+
},
|
|
3769
|
+
preflight_overrides: async (input) => {
|
|
3770
|
+
const db = getDb();
|
|
3771
|
+
const sessionId = getSessionId(input);
|
|
3772
|
+
if (!sessionId) {
|
|
3773
|
+
throw new Error('sessionId is required');
|
|
3774
|
+
}
|
|
3775
|
+
const preflight = buildOverridePreflight({
|
|
3776
|
+
db,
|
|
3777
|
+
sessionId,
|
|
3778
|
+
profileId: input.profileId,
|
|
3779
|
+
getSessionConnectionState,
|
|
3780
|
+
});
|
|
3781
|
+
return {
|
|
3782
|
+
...createBaseResponse(sessionId),
|
|
3783
|
+
...preflight,
|
|
3784
|
+
};
|
|
3785
|
+
},
|
|
3786
|
+
list_observed_override_assets: async (input) => {
|
|
3787
|
+
const sessionId = getSessionId(input);
|
|
3788
|
+
if (!sessionId) {
|
|
3789
|
+
throw new Error('sessionId is required');
|
|
3790
|
+
}
|
|
3791
|
+
const assets = listObservedOverrideAssets(getDb(), {
|
|
3792
|
+
sessionId,
|
|
3793
|
+
limit: typeof input.limit === 'number' ? input.limit : undefined,
|
|
3794
|
+
sinceTimestamp: typeof input.sinceTimestamp === 'number' ? input.sinceTimestamp : undefined,
|
|
3795
|
+
});
|
|
3796
|
+
return {
|
|
3797
|
+
...createBaseResponse(sessionId),
|
|
3798
|
+
limitsApplied: {
|
|
3799
|
+
maxResults: assets.length,
|
|
3800
|
+
truncated: false,
|
|
3801
|
+
},
|
|
3802
|
+
assets,
|
|
3803
|
+
};
|
|
3804
|
+
},
|
|
3805
|
+
plan_override_response_patch: async (input) => {
|
|
3806
|
+
const sessionId = getSessionId(input);
|
|
3807
|
+
const plan = planOverrideResponsePatch(input);
|
|
3808
|
+
const variantContext = buildOverrideVariantContext({
|
|
3809
|
+
targetUrl: plan.targetUrl,
|
|
3810
|
+
requestMethod: plan.requestMethod,
|
|
3811
|
+
matchMode: plan.matchMode,
|
|
3812
|
+
ruleType: plan.ruleType,
|
|
3813
|
+
captureMode: input.captureMode,
|
|
3814
|
+
source: input.source,
|
|
3815
|
+
triggerReload: input.triggerReload,
|
|
3816
|
+
requestHeaders: input.requestHeaders,
|
|
3817
|
+
});
|
|
3818
|
+
const auditPlan = persistResponsePlanAudit({
|
|
3819
|
+
db: getDb(),
|
|
3820
|
+
sessionId,
|
|
3821
|
+
input,
|
|
3822
|
+
plan,
|
|
3823
|
+
variantContext,
|
|
3824
|
+
});
|
|
3825
|
+
return {
|
|
3826
|
+
...createBaseResponse(sessionId),
|
|
3827
|
+
limitsApplied: {
|
|
3828
|
+
maxResults: plan.rule ? 1 : 0,
|
|
3829
|
+
truncated: false,
|
|
3830
|
+
},
|
|
3831
|
+
variantContext,
|
|
3832
|
+
audit: {
|
|
3833
|
+
persisted: auditPlan !== undefined,
|
|
3834
|
+
plans: auditPlan ? [auditPlan] : [],
|
|
3835
|
+
},
|
|
3836
|
+
...plan,
|
|
3837
|
+
};
|
|
3838
|
+
},
|
|
3839
|
+
map_next_override_assets: async (input) => {
|
|
3840
|
+
const projectRoot = normalizeOptionalString(input.projectRoot);
|
|
3841
|
+
if (!projectRoot) {
|
|
3842
|
+
throw new Error('projectRoot is required');
|
|
3843
|
+
}
|
|
3844
|
+
const sessionId = getSessionId(input);
|
|
3845
|
+
const observedAssets = Array.isArray(input.observedAssets)
|
|
3846
|
+
? input.observedAssets
|
|
3847
|
+
: sessionId
|
|
3848
|
+
? listObservedOverrideAssets(getDb(), { sessionId })
|
|
3849
|
+
: input.observedAssets;
|
|
3850
|
+
const mapping = await mapNextOverrideAssetsWithDrift({
|
|
3851
|
+
projectRoot,
|
|
3852
|
+
nextDir: normalizeOptionalString(input.nextDir),
|
|
3853
|
+
observedAssets,
|
|
3854
|
+
sourcePaths: input.sourcePaths,
|
|
3855
|
+
route: input.route,
|
|
3856
|
+
maxResults: input.maxResults,
|
|
3857
|
+
fetchProductionAssets: input.fetchProductionAssets,
|
|
3858
|
+
productionFetchTimeoutMs: input.productionFetchTimeoutMs,
|
|
3859
|
+
maxProductionAssetBytes: input.maxProductionAssetBytes,
|
|
3860
|
+
maxDriftCandidates: input.maxDriftCandidates,
|
|
3861
|
+
productionFetchConcurrency: input.productionFetchConcurrency,
|
|
3862
|
+
});
|
|
3863
|
+
return {
|
|
3864
|
+
...createBaseResponse(sessionId),
|
|
3865
|
+
limitsApplied: {
|
|
3866
|
+
maxResults: mapping.candidates.length,
|
|
3867
|
+
truncated: false,
|
|
3868
|
+
},
|
|
3869
|
+
observedFromPersisted: !Array.isArray(input.observedAssets) && sessionId
|
|
3870
|
+
? { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 }
|
|
3871
|
+
: undefined,
|
|
3872
|
+
...mapping,
|
|
3873
|
+
};
|
|
3874
|
+
},
|
|
3875
|
+
plan_next_source_override: async (input) => {
|
|
3876
|
+
const projectRoot = normalizeOptionalString(input.projectRoot);
|
|
3877
|
+
if (!projectRoot) {
|
|
3878
|
+
throw new Error('projectRoot is required');
|
|
3879
|
+
}
|
|
3880
|
+
const sessionId = getSessionId(input);
|
|
3881
|
+
const observedAssets = Array.isArray(input.observedAssets)
|
|
3882
|
+
? input.observedAssets
|
|
3883
|
+
: sessionId
|
|
3884
|
+
? listObservedOverrideAssets(getDb(), { sessionId })
|
|
3885
|
+
: input.observedAssets;
|
|
3886
|
+
const plan = await planNextSourceOverride({
|
|
3887
|
+
projectRoot,
|
|
3888
|
+
nextDir: normalizeOptionalString(input.nextDir),
|
|
3889
|
+
observedAssets,
|
|
3890
|
+
sourceEdits: input.sourceEdits,
|
|
3891
|
+
sourcePaths: input.sourcePaths,
|
|
3892
|
+
route: input.route,
|
|
3893
|
+
configPath: input.configPath,
|
|
3894
|
+
writeConfig: input.writeConfig,
|
|
3895
|
+
overwrite: input.overwrite,
|
|
3896
|
+
enabled: input.enabled,
|
|
3897
|
+
profileEnabled: input.profileEnabled,
|
|
3898
|
+
autoReload: input.autoReload,
|
|
3899
|
+
profileId: input.profileId,
|
|
3900
|
+
profileName: input.profileName,
|
|
3901
|
+
buildTimeoutMs: input.buildTimeoutMs,
|
|
3902
|
+
maxRules: input.maxRules,
|
|
3903
|
+
fetchProductionAssets: input.fetchProductionAssets,
|
|
3904
|
+
productionFetchTimeoutMs: input.productionFetchTimeoutMs,
|
|
3905
|
+
maxProductionAssetBytes: input.maxProductionAssetBytes,
|
|
3906
|
+
maxDriftCandidates: input.maxDriftCandidates,
|
|
3907
|
+
productionFetchConcurrency: input.productionFetchConcurrency,
|
|
3908
|
+
overlayTtlMs: input.overlayTtlMs,
|
|
3909
|
+
});
|
|
3910
|
+
const auditPlans = persistNextSourcePlanAudits({
|
|
3911
|
+
db: getDb(),
|
|
3912
|
+
sessionId,
|
|
3913
|
+
input,
|
|
3914
|
+
plan,
|
|
3915
|
+
});
|
|
3916
|
+
return {
|
|
3917
|
+
...createBaseResponse(sessionId),
|
|
3918
|
+
limitsApplied: {
|
|
3919
|
+
maxResults: plan.rules.length,
|
|
3920
|
+
truncated: false,
|
|
3921
|
+
},
|
|
3922
|
+
observedFromPersisted: !Array.isArray(input.observedAssets) && sessionId
|
|
3923
|
+
? { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 }
|
|
3924
|
+
: undefined,
|
|
3925
|
+
audit: {
|
|
3926
|
+
persisted: auditPlans.length > 0,
|
|
3927
|
+
plans: auditPlans,
|
|
3928
|
+
},
|
|
3929
|
+
...plan,
|
|
3930
|
+
};
|
|
3931
|
+
},
|
|
3932
|
+
get_override_status: async (input) => {
|
|
3933
|
+
const db = getDb();
|
|
3934
|
+
const sessionId = getSessionId(input);
|
|
3935
|
+
const profile = resolveOverrideProfileRecord(input.profileId);
|
|
3936
|
+
const latestRun = sessionId ? listOverridePocRuns(db, sessionId, 1, 0).runs[0] ?? null : null;
|
|
3937
|
+
const recentRequests = sessionId
|
|
3938
|
+
? listOverridePocRequests(db, sessionId, 5, 0, latestRun?.runId).requests
|
|
3939
|
+
: [];
|
|
3940
|
+
const recentPlans = sessionId
|
|
3941
|
+
? listOverridePlanAudits(db, { sessionId, limit: 5, offset: 0 }).plans
|
|
3942
|
+
: [];
|
|
3943
|
+
return {
|
|
3944
|
+
...createBaseResponse(sessionId),
|
|
3945
|
+
profile,
|
|
3946
|
+
latestRun,
|
|
3947
|
+
recentRequests,
|
|
3948
|
+
recentPlans,
|
|
3949
|
+
preflight: sessionId
|
|
3950
|
+
? buildOverridePreflight({
|
|
3951
|
+
db,
|
|
3952
|
+
sessionId,
|
|
3953
|
+
profileId: input.profileId,
|
|
3954
|
+
getSessionConnectionState,
|
|
3955
|
+
})
|
|
3956
|
+
: null,
|
|
3957
|
+
diagnosis: sessionId ? diagnoseOverridePoc(db, sessionId, latestRun?.runId) : null,
|
|
3958
|
+
nextActions: latestRun?.lastErrorCode
|
|
3959
|
+
? [{ code: 'DIAGNOSE_OVERRIDES', message: 'Run diagnose_overrides for the latest failed override run.' }]
|
|
3960
|
+
: latestRun
|
|
3961
|
+
? [{ code: 'GET_OVERRIDE_REQUEST_LOG', message: 'Inspect get_override_request_log for matched and fulfilled requests.' }]
|
|
3962
|
+
: [{ code: 'ENABLE_OVERRIDES', message: 'Enable overrides on a connected session after profile validation succeeds.' }],
|
|
3963
|
+
};
|
|
3964
|
+
},
|
|
3965
|
+
get_override_request_log: async (input) => {
|
|
3966
|
+
const db = getDb();
|
|
3967
|
+
const sessionId = getSessionId(input);
|
|
3968
|
+
if (!sessionId) {
|
|
3969
|
+
throw new Error('sessionId is required');
|
|
3970
|
+
}
|
|
3971
|
+
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
3972
|
+
const offset = resolveOffset(input.offset);
|
|
3973
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
3974
|
+
const runId = typeof input.runId === 'string' && input.runId.trim().length > 0
|
|
3975
|
+
? input.runId.trim()
|
|
3976
|
+
: undefined;
|
|
3977
|
+
const result = listOverridePocRequests(db, sessionId, limit, offset, runId);
|
|
3978
|
+
const bytePage = applyByteBudget(result.requests, maxResponseBytes);
|
|
3979
|
+
const truncated = result.hasMore || bytePage.truncatedByBytes;
|
|
3980
|
+
return {
|
|
3981
|
+
...createBaseResponse(sessionId),
|
|
3982
|
+
limitsApplied: {
|
|
3983
|
+
maxResults: limit,
|
|
3984
|
+
truncated,
|
|
3985
|
+
},
|
|
3986
|
+
runId: runId ?? null,
|
|
3987
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
3988
|
+
responseBytes: bytePage.responseBytes,
|
|
3989
|
+
requests: bytePage.items,
|
|
3990
|
+
nextActions: bytePage.items.length === 0
|
|
3991
|
+
? [{ code: 'RELOAD_TAB', message: 'Reload the selected tab after enabling overrides so matching requests are observed.' }]
|
|
3992
|
+
: [{ code: 'DIAGNOSE_OVERRIDES', message: 'Run diagnose_overrides if any matched request failed or did not fulfill.' }],
|
|
3993
|
+
};
|
|
3994
|
+
},
|
|
3995
|
+
get_override_plan_log: async (input) => {
|
|
3996
|
+
const db = getDb();
|
|
3997
|
+
const sessionId = getSessionId(input);
|
|
3998
|
+
if (!sessionId) {
|
|
3999
|
+
throw new Error('sessionId is required');
|
|
4000
|
+
}
|
|
4001
|
+
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
4002
|
+
const offset = resolveOffset(input.offset);
|
|
4003
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
4004
|
+
const planId = typeof input.planId === 'string' && input.planId.trim().length > 0
|
|
4005
|
+
? input.planId.trim()
|
|
4006
|
+
: undefined;
|
|
4007
|
+
const result = listOverridePlanAudits(db, { sessionId, limit, offset, planId });
|
|
4008
|
+
const bytePage = applyByteBudget(result.plans, maxResponseBytes);
|
|
4009
|
+
const truncated = result.hasMore || bytePage.truncatedByBytes;
|
|
4010
|
+
return {
|
|
4011
|
+
...createBaseResponse(sessionId),
|
|
4012
|
+
limitsApplied: {
|
|
4013
|
+
maxResults: limit,
|
|
4014
|
+
truncated,
|
|
4015
|
+
},
|
|
4016
|
+
planId: planId ?? null,
|
|
4017
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
4018
|
+
responseBytes: bytePage.responseBytes,
|
|
4019
|
+
plans: bytePage.items,
|
|
4020
|
+
nextActions: bytePage.items.length === 0
|
|
4021
|
+
? [{ code: 'PLAN_OVERRIDE', message: 'Run plan_override_response_patch or plan_next_source_override with sessionId to persist generated rule metadata.' }]
|
|
4022
|
+
: [{ code: 'REVIEW_ROLLBACK', message: 'Review rollback metadata before enabling or deleting generated override files.' }],
|
|
4023
|
+
};
|
|
4024
|
+
},
|
|
4025
|
+
diagnose_overrides: async (input) => {
|
|
4026
|
+
const db = getDb();
|
|
4027
|
+
const sessionId = getSessionId(input);
|
|
4028
|
+
if (!sessionId) {
|
|
4029
|
+
throw new Error('sessionId is required');
|
|
4030
|
+
}
|
|
4031
|
+
const runId = typeof input.runId === 'string' && input.runId.trim().length > 0
|
|
4032
|
+
? input.runId.trim()
|
|
4033
|
+
: undefined;
|
|
4034
|
+
const diagnosis = diagnoseOverridePoc(db, sessionId, runId);
|
|
4035
|
+
const firstIssue = diagnosis.issues[0];
|
|
4036
|
+
return {
|
|
4037
|
+
...createBaseResponse(sessionId),
|
|
4038
|
+
diagnosis,
|
|
4039
|
+
nextActions: firstIssue?.suggestedActions[0]
|
|
4040
|
+
? [{ code: firstIssue.code, message: firstIssue.suggestedActions[0] }]
|
|
4041
|
+
: [{ code: 'NO_DIAGNOSIS_ISSUES', message: 'No diagnosis issues were found for the selected override run.' }],
|
|
4042
|
+
};
|
|
4043
|
+
},
|
|
4044
|
+
get_recent_events: async (input) => {
|
|
4045
|
+
const db = getDb();
|
|
4046
|
+
const sessionId = getSessionId(input);
|
|
4047
|
+
const origin = normalizeRequestedOrigin(input.url);
|
|
4048
|
+
ensureSessionOrOriginFilter(sessionId, origin);
|
|
4049
|
+
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
4050
|
+
const offset = resolveOffset(input.offset);
|
|
4051
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
4052
|
+
const responseProfile = resolveResponseProfile(input.responseProfile);
|
|
4053
|
+
const includePayload = responseProfile === 'compact' && input.includePayload === true;
|
|
4054
|
+
const requestedTypes = parseRequestedTypes(input.types ?? input.eventTypes);
|
|
4055
|
+
const params = [];
|
|
4056
|
+
const where = [];
|
|
4057
|
+
if (sessionId) {
|
|
4058
|
+
where.push('session_id = ?');
|
|
4059
|
+
params.push(sessionId);
|
|
4060
|
+
}
|
|
4061
|
+
appendEventOriginFilter(where, params, origin);
|
|
4062
|
+
if (requestedTypes.length > 0) {
|
|
4063
|
+
const placeholders = requestedTypes.map(() => '?').join(', ');
|
|
4064
|
+
where.push(`type IN (${placeholders})`);
|
|
4065
|
+
params.push(...requestedTypes);
|
|
4066
|
+
}
|
|
4067
|
+
const rows = db
|
|
4068
|
+
.prepare(`
|
|
4069
|
+
SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
|
|
4070
|
+
FROM events
|
|
4071
|
+
WHERE ${where.join(' AND ')}
|
|
4072
|
+
ORDER BY ts DESC
|
|
1210
4073
|
LIMIT ? OFFSET ?
|
|
1211
4074
|
`)
|
|
1212
4075
|
.all(...params, limit + 1, offset);
|
|
@@ -2195,163 +5058,761 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
2195
5058
|
return {
|
|
2196
5059
|
...createBaseResponse(sessionId),
|
|
2197
5060
|
limitsApplied: {
|
|
2198
|
-
maxResults: limit,
|
|
2199
|
-
truncated,
|
|
5061
|
+
maxResults: limit,
|
|
5062
|
+
truncated,
|
|
5063
|
+
},
|
|
5064
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
5065
|
+
responseBytes: bytePage.responseBytes,
|
|
5066
|
+
snapshots: bytePage.items,
|
|
5067
|
+
};
|
|
5068
|
+
},
|
|
5069
|
+
get_snapshot_for_event: async (input) => {
|
|
5070
|
+
const db = getDb();
|
|
5071
|
+
const sessionId = getSessionId(input);
|
|
5072
|
+
if (!sessionId) {
|
|
5073
|
+
throw new Error('sessionId is required');
|
|
5074
|
+
}
|
|
5075
|
+
const eventId = typeof input.eventId === 'string' ? input.eventId : '';
|
|
5076
|
+
if (!eventId) {
|
|
5077
|
+
throw new Error('eventId is required');
|
|
5078
|
+
}
|
|
5079
|
+
const maxDeltaMs = resolveDurationMs(input.maxDeltaMs, 10_000, 60_000);
|
|
5080
|
+
const event = db
|
|
5081
|
+
.prepare('SELECT event_id, ts, type FROM events WHERE session_id = ? AND event_id = ? LIMIT 1')
|
|
5082
|
+
.get(sessionId, eventId);
|
|
5083
|
+
if (!event) {
|
|
5084
|
+
throw new Error(`Event not found: ${eventId}`);
|
|
5085
|
+
}
|
|
5086
|
+
const byTriggerLink = db
|
|
5087
|
+
.prepare(`SELECT
|
|
5088
|
+
snapshot_id, session_id, trigger_event_id, ts, trigger, selector, url, mode, style_mode,
|
|
5089
|
+
dom_json, styles_json, png_path, png_mime, png_bytes,
|
|
5090
|
+
dom_truncated, styles_truncated, png_truncated, created_at
|
|
5091
|
+
FROM snapshots
|
|
5092
|
+
WHERE session_id = ? AND trigger_event_id = ?
|
|
5093
|
+
ORDER BY ts ASC
|
|
5094
|
+
LIMIT 1`)
|
|
5095
|
+
.get(sessionId, eventId);
|
|
5096
|
+
if (byTriggerLink) {
|
|
5097
|
+
return {
|
|
5098
|
+
...createBaseResponse(sessionId),
|
|
5099
|
+
limitsApplied: {
|
|
5100
|
+
maxResults: 1,
|
|
5101
|
+
truncated: false,
|
|
5102
|
+
},
|
|
5103
|
+
event: {
|
|
5104
|
+
eventId: event.event_id,
|
|
5105
|
+
timestamp: event.ts,
|
|
5106
|
+
type: event.type,
|
|
5107
|
+
},
|
|
5108
|
+
matchReason: 'trigger_event_id',
|
|
5109
|
+
snapshot: mapSnapshotMetadata(byTriggerLink),
|
|
5110
|
+
};
|
|
5111
|
+
}
|
|
5112
|
+
const byTimestamp = db
|
|
5113
|
+
.prepare(`SELECT
|
|
5114
|
+
snapshot_id, session_id, trigger_event_id, ts, trigger, selector, url, mode, style_mode,
|
|
5115
|
+
dom_json, styles_json, png_path, png_mime, png_bytes,
|
|
5116
|
+
dom_truncated, styles_truncated, png_truncated, created_at
|
|
5117
|
+
FROM snapshots
|
|
5118
|
+
WHERE session_id = ? AND ts BETWEEN ? AND ?
|
|
5119
|
+
ORDER BY ABS(ts - ?) ASC, ts ASC
|
|
5120
|
+
LIMIT 1`)
|
|
5121
|
+
.get(sessionId, event.ts, event.ts + maxDeltaMs, event.ts);
|
|
5122
|
+
return {
|
|
5123
|
+
...createBaseResponse(sessionId),
|
|
5124
|
+
limitsApplied: {
|
|
5125
|
+
maxResults: 1,
|
|
5126
|
+
truncated: false,
|
|
5127
|
+
},
|
|
5128
|
+
event: {
|
|
5129
|
+
eventId: event.event_id,
|
|
5130
|
+
timestamp: event.ts,
|
|
5131
|
+
type: event.type,
|
|
5132
|
+
},
|
|
5133
|
+
matchReason: byTimestamp ? 'nearest_timestamp' : 'none',
|
|
5134
|
+
snapshot: byTimestamp ? mapSnapshotMetadata(byTimestamp) : null,
|
|
5135
|
+
};
|
|
5136
|
+
},
|
|
5137
|
+
get_snapshot_asset: async (input) => {
|
|
5138
|
+
const db = getDb();
|
|
5139
|
+
const sessionId = getSessionId(input);
|
|
5140
|
+
if (!sessionId) {
|
|
5141
|
+
throw new Error('sessionId is required');
|
|
5142
|
+
}
|
|
5143
|
+
const snapshotId = typeof input.snapshotId === 'string' ? input.snapshotId : '';
|
|
5144
|
+
if (!snapshotId) {
|
|
5145
|
+
throw new Error('snapshotId is required');
|
|
5146
|
+
}
|
|
5147
|
+
const assetType = input.asset === 'png' ? 'png' : 'png';
|
|
5148
|
+
const encoding = input.encoding === 'raw' ? 'raw' : 'base64';
|
|
5149
|
+
const offset = resolveOffset(input.offset);
|
|
5150
|
+
const maxBytes = resolveChunkBytes(input.maxBytes, DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES);
|
|
5151
|
+
const snapshot = db
|
|
5152
|
+
.prepare(`SELECT snapshot_id, session_id, png_path, png_mime, png_bytes
|
|
5153
|
+
FROM snapshots
|
|
5154
|
+
WHERE session_id = ? AND snapshot_id = ?
|
|
5155
|
+
LIMIT 1`)
|
|
5156
|
+
.get(sessionId, snapshotId);
|
|
5157
|
+
if (!snapshot) {
|
|
5158
|
+
throw new Error(`Snapshot not found: ${snapshotId}`);
|
|
5159
|
+
}
|
|
5160
|
+
if (assetType !== 'png' || !snapshot.png_path) {
|
|
5161
|
+
throw new Error('Requested snapshot asset is not available.');
|
|
5162
|
+
}
|
|
5163
|
+
const dbPath = getMainDbPath(db);
|
|
5164
|
+
const absolutePath = resolveSnapshotAbsolutePath(dbPath, snapshot.png_path);
|
|
5165
|
+
if (!existsSync(absolutePath)) {
|
|
5166
|
+
throw new Error(`Snapshot asset is missing on disk: ${snapshot.png_path}`);
|
|
5167
|
+
}
|
|
5168
|
+
const fullBuffer = readFileSync(absolutePath);
|
|
5169
|
+
if (offset >= fullBuffer.byteLength) {
|
|
5170
|
+
return {
|
|
5171
|
+
...createBaseResponse(sessionId),
|
|
5172
|
+
limitsApplied: {
|
|
5173
|
+
maxResults: maxBytes,
|
|
5174
|
+
truncated: false,
|
|
5175
|
+
},
|
|
5176
|
+
snapshotId,
|
|
5177
|
+
asset: assetType,
|
|
5178
|
+
assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
|
|
5179
|
+
mime: snapshot.png_mime ?? 'image/png',
|
|
5180
|
+
totalBytes: fullBuffer.byteLength,
|
|
5181
|
+
offset,
|
|
5182
|
+
returnedBytes: 0,
|
|
5183
|
+
hasMore: false,
|
|
5184
|
+
nextOffset: null,
|
|
5185
|
+
encoding,
|
|
5186
|
+
chunk: encoding === 'raw' ? [] : undefined,
|
|
5187
|
+
chunkBase64: encoding === 'base64' ? '' : undefined,
|
|
5188
|
+
};
|
|
5189
|
+
}
|
|
5190
|
+
const chunkBuffer = fullBuffer.subarray(offset, Math.min(offset + maxBytes, fullBuffer.byteLength));
|
|
5191
|
+
const returnedBytes = chunkBuffer.byteLength;
|
|
5192
|
+
const nextOffset = offset + returnedBytes;
|
|
5193
|
+
const hasMore = nextOffset < fullBuffer.byteLength;
|
|
5194
|
+
return {
|
|
5195
|
+
...createBaseResponse(sessionId),
|
|
5196
|
+
limitsApplied: {
|
|
5197
|
+
maxResults: maxBytes,
|
|
5198
|
+
truncated: hasMore,
|
|
5199
|
+
},
|
|
5200
|
+
snapshotId,
|
|
5201
|
+
asset: assetType,
|
|
5202
|
+
assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
|
|
5203
|
+
mime: snapshot.png_mime ?? 'image/png',
|
|
5204
|
+
totalBytes: fullBuffer.byteLength,
|
|
5205
|
+
offset,
|
|
5206
|
+
returnedBytes,
|
|
5207
|
+
hasMore,
|
|
5208
|
+
nextOffset: hasMore ? nextOffset : null,
|
|
5209
|
+
encoding,
|
|
5210
|
+
chunk: encoding === 'raw' ? Array.from(chunkBuffer.values()) : undefined,
|
|
5211
|
+
chunkBase64: encoding === 'base64' ? chunkBuffer.toString('base64') : undefined,
|
|
5212
|
+
};
|
|
5213
|
+
},
|
|
5214
|
+
list_automation_runs: async (input) => {
|
|
5215
|
+
const db = getDb();
|
|
5216
|
+
const sessionId = getSessionId(input);
|
|
5217
|
+
if (!sessionId) {
|
|
5218
|
+
throw new Error('sessionId is required');
|
|
5219
|
+
}
|
|
5220
|
+
const status = normalizeOptionalString(input.status);
|
|
5221
|
+
const action = normalizeOptionalString(input.action);
|
|
5222
|
+
const traceId = normalizeOptionalString(input.traceId);
|
|
5223
|
+
const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
|
|
5224
|
+
const offset = resolveOffset(input.offset);
|
|
5225
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
5226
|
+
const where = ['r.session_id = ?'];
|
|
5227
|
+
const params = [sessionId];
|
|
5228
|
+
if (status) {
|
|
5229
|
+
where.push('r.status = ?');
|
|
5230
|
+
params.push(status);
|
|
5231
|
+
}
|
|
5232
|
+
if (action) {
|
|
5233
|
+
where.push('r.action = ?');
|
|
5234
|
+
params.push(action);
|
|
5235
|
+
}
|
|
5236
|
+
if (traceId) {
|
|
5237
|
+
where.push('r.trace_id = ?');
|
|
5238
|
+
params.push(traceId);
|
|
5239
|
+
}
|
|
5240
|
+
const rows = db.prepare(`SELECT
|
|
5241
|
+
r.run_id,
|
|
5242
|
+
r.session_id,
|
|
5243
|
+
r.trace_id,
|
|
5244
|
+
r.action,
|
|
5245
|
+
r.tab_id,
|
|
5246
|
+
r.selector,
|
|
5247
|
+
r.status,
|
|
5248
|
+
r.started_at,
|
|
5249
|
+
r.completed_at,
|
|
5250
|
+
r.stop_reason,
|
|
5251
|
+
r.target_summary_json,
|
|
5252
|
+
r.failure_json,
|
|
5253
|
+
r.redaction_json,
|
|
5254
|
+
r.created_at,
|
|
5255
|
+
r.updated_at,
|
|
5256
|
+
COALESCE(step_stats.step_count, 0) AS step_count,
|
|
5257
|
+
step_stats.last_step_at
|
|
5258
|
+
FROM automation_runs r
|
|
5259
|
+
LEFT JOIN (
|
|
5260
|
+
SELECT
|
|
5261
|
+
run_id,
|
|
5262
|
+
COUNT(*) AS step_count,
|
|
5263
|
+
MAX(COALESCE(finished_at, started_at, created_at)) AS last_step_at
|
|
5264
|
+
FROM automation_steps
|
|
5265
|
+
GROUP BY run_id
|
|
5266
|
+
) step_stats ON step_stats.run_id = r.run_id
|
|
5267
|
+
WHERE ${where.join(' AND ')}
|
|
5268
|
+
ORDER BY r.started_at DESC, r.run_id DESC
|
|
5269
|
+
LIMIT ? OFFSET ?`).all(...params, limit + 1, offset);
|
|
5270
|
+
const truncatedByLimit = rows.length > limit;
|
|
5271
|
+
const runs = rows.slice(0, limit).map((row) => mapAutomationRunRecord(row));
|
|
5272
|
+
const bytePage = applyByteBudget(runs, maxResponseBytes);
|
|
5273
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
5274
|
+
return {
|
|
5275
|
+
...createBaseResponse(sessionId),
|
|
5276
|
+
limitsApplied: {
|
|
5277
|
+
maxResults: limit,
|
|
5278
|
+
truncated,
|
|
5279
|
+
},
|
|
5280
|
+
filtersApplied: {
|
|
5281
|
+
sessionId,
|
|
5282
|
+
status,
|
|
5283
|
+
action,
|
|
5284
|
+
traceId,
|
|
5285
|
+
},
|
|
5286
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
5287
|
+
responseBytes: bytePage.responseBytes,
|
|
5288
|
+
runs: bytePage.items,
|
|
5289
|
+
};
|
|
5290
|
+
},
|
|
5291
|
+
get_automation_run: async (input) => {
|
|
5292
|
+
const db = getDb();
|
|
5293
|
+
const sessionId = getSessionId(input);
|
|
5294
|
+
if (!sessionId) {
|
|
5295
|
+
throw new Error('sessionId is required');
|
|
5296
|
+
}
|
|
5297
|
+
const runId = normalizeOptionalString(input.runId);
|
|
5298
|
+
if (!runId) {
|
|
5299
|
+
throw new Error('runId is required');
|
|
5300
|
+
}
|
|
5301
|
+
const stepLimit = resolveLimit(input.stepLimit, DEFAULT_LIST_LIMIT);
|
|
5302
|
+
const stepOffset = resolveOffset(input.stepOffset);
|
|
5303
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
5304
|
+
const run = db.prepare(`SELECT
|
|
5305
|
+
r.run_id,
|
|
5306
|
+
r.session_id,
|
|
5307
|
+
r.trace_id,
|
|
5308
|
+
r.action,
|
|
5309
|
+
r.tab_id,
|
|
5310
|
+
r.selector,
|
|
5311
|
+
r.status,
|
|
5312
|
+
r.started_at,
|
|
5313
|
+
r.completed_at,
|
|
5314
|
+
r.stop_reason,
|
|
5315
|
+
r.target_summary_json,
|
|
5316
|
+
r.failure_json,
|
|
5317
|
+
r.redaction_json,
|
|
5318
|
+
r.created_at,
|
|
5319
|
+
r.updated_at,
|
|
5320
|
+
COALESCE(step_stats.step_count, 0) AS step_count,
|
|
5321
|
+
step_stats.last_step_at
|
|
5322
|
+
FROM automation_runs r
|
|
5323
|
+
LEFT JOIN (
|
|
5324
|
+
SELECT
|
|
5325
|
+
run_id,
|
|
5326
|
+
COUNT(*) AS step_count,
|
|
5327
|
+
MAX(COALESCE(finished_at, started_at, created_at)) AS last_step_at
|
|
5328
|
+
FROM automation_steps
|
|
5329
|
+
GROUP BY run_id
|
|
5330
|
+
) step_stats ON step_stats.run_id = r.run_id
|
|
5331
|
+
WHERE r.session_id = ? AND r.run_id = ?
|
|
5332
|
+
LIMIT 1`).get(sessionId, runId);
|
|
5333
|
+
if (!run) {
|
|
5334
|
+
throw new Error(`Automation run not found: ${runId}`);
|
|
5335
|
+
}
|
|
5336
|
+
const stepRows = db.prepare(`SELECT
|
|
5337
|
+
step_id,
|
|
5338
|
+
run_id,
|
|
5339
|
+
session_id,
|
|
5340
|
+
step_order,
|
|
5341
|
+
trace_id,
|
|
5342
|
+
action,
|
|
5343
|
+
selector,
|
|
5344
|
+
status,
|
|
5345
|
+
started_at,
|
|
5346
|
+
finished_at,
|
|
5347
|
+
duration_ms,
|
|
5348
|
+
tab_id,
|
|
5349
|
+
target_summary_json,
|
|
5350
|
+
redaction_json,
|
|
5351
|
+
failure_json,
|
|
5352
|
+
input_metadata_json,
|
|
5353
|
+
event_type,
|
|
5354
|
+
event_id,
|
|
5355
|
+
created_at,
|
|
5356
|
+
updated_at
|
|
5357
|
+
FROM automation_steps
|
|
5358
|
+
WHERE session_id = ? AND run_id = ?
|
|
5359
|
+
ORDER BY step_order ASC, created_at ASC
|
|
5360
|
+
LIMIT ? OFFSET ?`).all(sessionId, runId, stepLimit + 1, stepOffset);
|
|
5361
|
+
const truncatedByLimit = stepRows.length > stepLimit;
|
|
5362
|
+
const steps = stepRows.slice(0, stepLimit).map((row) => mapAutomationStepRecord(row));
|
|
5363
|
+
const bytePage = applyByteBudget(steps, maxResponseBytes);
|
|
5364
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
5365
|
+
return {
|
|
5366
|
+
...createBaseResponse(sessionId),
|
|
5367
|
+
limitsApplied: {
|
|
5368
|
+
maxResults: stepLimit,
|
|
5369
|
+
truncated,
|
|
5370
|
+
},
|
|
5371
|
+
run: mapAutomationRunRecord(run),
|
|
5372
|
+
steps: bytePage.items,
|
|
5373
|
+
pagination: buildOffsetPagination(stepOffset, bytePage.items.length, truncated, maxResponseBytes),
|
|
5374
|
+
responseBytes: bytePage.responseBytes,
|
|
5375
|
+
};
|
|
5376
|
+
},
|
|
5377
|
+
};
|
|
5378
|
+
}
|
|
5379
|
+
export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionState) {
|
|
5380
|
+
const capturePageState = async (sessionId, input) => {
|
|
5381
|
+
const maxItems = resolveStructuredMaxItems(input.maxItems, 40);
|
|
5382
|
+
const maxTextLength = resolveStructuredTextLength(input.maxTextLength, 80);
|
|
5383
|
+
const includeButtons = input.includeButtons !== false;
|
|
5384
|
+
const includeInputs = input.includeInputs !== false;
|
|
5385
|
+
const includeModals = input.includeModals !== false;
|
|
5386
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_PAGE_STATE', {
|
|
5387
|
+
maxItems,
|
|
5388
|
+
maxTextLength,
|
|
5389
|
+
includeButtons,
|
|
5390
|
+
includeInputs,
|
|
5391
|
+
includeModals,
|
|
5392
|
+
}, 4_000);
|
|
5393
|
+
return {
|
|
5394
|
+
limitsApplied: {
|
|
5395
|
+
maxResults: maxItems,
|
|
5396
|
+
truncated: capture.truncated ?? false,
|
|
5397
|
+
},
|
|
5398
|
+
payload: ensureCaptureSuccess(capture, sessionId),
|
|
5399
|
+
};
|
|
5400
|
+
};
|
|
5401
|
+
return {
|
|
5402
|
+
observe_override_assets: async (input) => {
|
|
5403
|
+
const sessionId = getSessionId(input);
|
|
5404
|
+
if (!sessionId) {
|
|
5405
|
+
throw new Error('sessionId is required');
|
|
5406
|
+
}
|
|
5407
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
5408
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_OBSERVE_ASSETS', { tabId, includePerformance: input.includePerformance !== false }, 5_000);
|
|
5409
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
5410
|
+
const assetCount = Array.isArray(payload.assets) ? payload.assets.length : 0;
|
|
5411
|
+
const persisted = getDb
|
|
5412
|
+
? persistObservedOverrideAssets(getDb(), { ...payload, sessionId, tabId: payload.tabId ?? tabId })
|
|
5413
|
+
: undefined;
|
|
5414
|
+
return {
|
|
5415
|
+
...createBaseResponse(sessionId),
|
|
5416
|
+
limitsApplied: {
|
|
5417
|
+
maxResults: assetCount,
|
|
5418
|
+
truncated: capture.truncated ?? false,
|
|
5419
|
+
},
|
|
5420
|
+
persisted,
|
|
5421
|
+
...payload,
|
|
5422
|
+
nextActions: assetCount > 0
|
|
5423
|
+
? [{ code: 'MAP_NEXT_ASSETS', message: 'Run map_next_override_assets with projectRoot and sourcePaths to score override candidates.' }]
|
|
5424
|
+
: [{ code: 'LOAD_ROUTE', message: 'Load or interact with the target route so document, asset, and fetch resources are requested, then observe again.' }],
|
|
5425
|
+
};
|
|
5426
|
+
},
|
|
5427
|
+
capture_override_response_body: async (input) => {
|
|
5428
|
+
const sessionId = getSessionId(input);
|
|
5429
|
+
if (!sessionId) {
|
|
5430
|
+
throw new Error('sessionId is required');
|
|
5431
|
+
}
|
|
5432
|
+
const targetUrl = normalizeOptionalString(input.targetUrl) ?? normalizeOptionalString(input.targetAssetUrl);
|
|
5433
|
+
if (!targetUrl) {
|
|
5434
|
+
throw new Error('targetUrl is required');
|
|
5435
|
+
}
|
|
5436
|
+
assertOverrideResponseRequestCaptureSafe({
|
|
5437
|
+
requestMethod: input.requestMethod,
|
|
5438
|
+
requestHeaders: input.requestHeaders,
|
|
5439
|
+
subject: 'Response body capture request',
|
|
5440
|
+
});
|
|
5441
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
5442
|
+
const timeoutMs = resolveTimeoutMs(input.timeoutMs, 10_000, 60_000);
|
|
5443
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_RESPONSE_BODY', {
|
|
5444
|
+
targetUrl,
|
|
5445
|
+
tabId,
|
|
5446
|
+
captureMode: normalizeOptionalString(input.captureMode),
|
|
5447
|
+
triggerReload: typeof input.triggerReload === 'boolean' ? input.triggerReload : undefined,
|
|
5448
|
+
matchMode: normalizeOptionalString(input.matchMode),
|
|
5449
|
+
requestMethod: input.requestMethod,
|
|
5450
|
+
requestHeaders: isRecord(input.requestHeaders) ? input.requestHeaders : undefined,
|
|
5451
|
+
timeoutMs,
|
|
5452
|
+
maxBodyBytes: input.maxBodyBytes,
|
|
5453
|
+
includeBody: input.includeBody === true,
|
|
5454
|
+
}, timeoutMs + 2_000);
|
|
5455
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
5456
|
+
return {
|
|
5457
|
+
...createBaseResponse(sessionId),
|
|
5458
|
+
limitsApplied: {
|
|
5459
|
+
maxResults: 1,
|
|
5460
|
+
truncated: capture.truncated ?? payload.truncated === true,
|
|
5461
|
+
},
|
|
5462
|
+
...payload,
|
|
5463
|
+
nextActions: payload.bodyCaptured === true
|
|
5464
|
+
? [{ code: 'PLAN_RESPONSE_PATCH', message: 'Run plan_override_response_patch with textPatches or jsonPatches to generate an exact response override.' }]
|
|
5465
|
+
: [{ code: 'UNSUPPORTED_RESPONSE_BODY', message: 'Only bounded text-like response bodies can be patched safely.' }],
|
|
5466
|
+
};
|
|
5467
|
+
},
|
|
5468
|
+
plan_override_response_patch: async (input) => {
|
|
5469
|
+
const sessionId = getSessionId(input);
|
|
5470
|
+
let plannerInput = input;
|
|
5471
|
+
let capturedFromLiveSession;
|
|
5472
|
+
const hasProvidedBody = typeof input.responseBodyText === 'string'
|
|
5473
|
+
|| typeof input.bodyText === 'string'
|
|
5474
|
+
|| typeof input.responseBodyBase64 === 'string'
|
|
5475
|
+
|| typeof input.bodyBase64 === 'string';
|
|
5476
|
+
if (!hasProvidedBody && sessionId) {
|
|
5477
|
+
const targetUrl = normalizeOptionalString(input.targetUrl) ?? normalizeOptionalString(input.targetAssetUrl);
|
|
5478
|
+
if (!targetUrl) {
|
|
5479
|
+
throw new Error('targetUrl is required');
|
|
5480
|
+
}
|
|
5481
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
5482
|
+
const timeoutMs = resolveTimeoutMs(input.timeoutMs, 10_000, 60_000);
|
|
5483
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_RESPONSE_BODY', {
|
|
5484
|
+
targetUrl,
|
|
5485
|
+
tabId,
|
|
5486
|
+
captureMode: normalizeOptionalString(input.captureMode),
|
|
5487
|
+
triggerReload: typeof input.triggerReload === 'boolean' ? input.triggerReload : undefined,
|
|
5488
|
+
matchMode: normalizeOptionalString(input.matchMode),
|
|
5489
|
+
requestMethod: input.requestMethod,
|
|
5490
|
+
requestHeaders: isRecord(input.requestHeaders) ? input.requestHeaders : undefined,
|
|
5491
|
+
timeoutMs,
|
|
5492
|
+
maxBodyBytes: input.maxBodyBytes,
|
|
5493
|
+
includeBody: true,
|
|
5494
|
+
}, timeoutMs + 2_000);
|
|
5495
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
5496
|
+
if (payload.truncated === true) {
|
|
5497
|
+
throw new Error('Captured response body was truncated; increase maxBodyBytes before planning a patch.');
|
|
5498
|
+
}
|
|
5499
|
+
if (typeof payload.bodyText !== 'string') {
|
|
5500
|
+
throw new Error('Captured response did not include a text body that can be patched.');
|
|
5501
|
+
}
|
|
5502
|
+
plannerInput = {
|
|
5503
|
+
...input,
|
|
5504
|
+
responseBodyText: payload.bodyText,
|
|
5505
|
+
contentType: input.contentType ?? payload.contentType,
|
|
5506
|
+
ruleType: input.ruleType ?? payload.ruleType,
|
|
5507
|
+
requestMethod: input.requestMethod ?? payload.requestMethod,
|
|
5508
|
+
captureMode: input.captureMode ?? payload.captureMode,
|
|
5509
|
+
source: payload.source,
|
|
5510
|
+
requestHeaders: payload.requestHeaders,
|
|
5511
|
+
};
|
|
5512
|
+
const variantContext = buildOverrideVariantContext({
|
|
5513
|
+
targetUrl: payload.targetUrl,
|
|
5514
|
+
requestMethod: input.requestMethod ?? payload.requestMethod,
|
|
5515
|
+
matchMode: payload.matchMode,
|
|
5516
|
+
ruleType: input.ruleType ?? payload.ruleType,
|
|
5517
|
+
captureMode: payload.captureMode,
|
|
5518
|
+
source: payload.source,
|
|
5519
|
+
triggerReload: payload.triggerReload,
|
|
5520
|
+
requestHeaders: payload.requestHeaders,
|
|
5521
|
+
});
|
|
5522
|
+
capturedFromLiveSession = {
|
|
5523
|
+
sessionId,
|
|
5524
|
+
targetUrl: payload.targetUrl,
|
|
5525
|
+
requestMethod: input.requestMethod ?? payload.requestMethod,
|
|
5526
|
+
statusCode: payload.statusCode,
|
|
5527
|
+
contentType: payload.contentType,
|
|
5528
|
+
bodyBytes: payload.bodyBytes,
|
|
5529
|
+
capturedBytes: payload.capturedBytes,
|
|
5530
|
+
truncated: payload.truncated === true,
|
|
5531
|
+
ruleType: payload.ruleType,
|
|
5532
|
+
matchMode: payload.matchMode,
|
|
5533
|
+
captureMode: payload.captureMode,
|
|
5534
|
+
source: payload.source,
|
|
5535
|
+
tabId: payload.tabId,
|
|
5536
|
+
triggerReload: payload.triggerReload,
|
|
5537
|
+
requestHeaders: payload.requestHeaders,
|
|
5538
|
+
variantContext,
|
|
5539
|
+
};
|
|
5540
|
+
}
|
|
5541
|
+
const plan = planOverrideResponsePatch(plannerInput);
|
|
5542
|
+
const variantContext = buildOverrideVariantContext({
|
|
5543
|
+
targetUrl: plan.targetUrl,
|
|
5544
|
+
requestMethod: plan.requestMethod,
|
|
5545
|
+
matchMode: plan.matchMode,
|
|
5546
|
+
ruleType: plan.ruleType,
|
|
5547
|
+
captureMode: plannerInput.captureMode,
|
|
5548
|
+
source: plannerInput.source,
|
|
5549
|
+
triggerReload: plannerInput.triggerReload,
|
|
5550
|
+
requestHeaders: plannerInput.requestHeaders,
|
|
5551
|
+
});
|
|
5552
|
+
const auditPlan = getDb
|
|
5553
|
+
? persistResponsePlanAudit({
|
|
5554
|
+
db: getDb(),
|
|
5555
|
+
sessionId,
|
|
5556
|
+
input,
|
|
5557
|
+
plan,
|
|
5558
|
+
capturedFromLiveSession,
|
|
5559
|
+
variantContext,
|
|
5560
|
+
})
|
|
5561
|
+
: undefined;
|
|
5562
|
+
return {
|
|
5563
|
+
...createBaseResponse(sessionId),
|
|
5564
|
+
limitsApplied: {
|
|
5565
|
+
maxResults: plan.rule ? 1 : 0,
|
|
5566
|
+
truncated: false,
|
|
5567
|
+
},
|
|
5568
|
+
capturedFromLiveSession,
|
|
5569
|
+
variantContext,
|
|
5570
|
+
audit: {
|
|
5571
|
+
persisted: auditPlan !== undefined,
|
|
5572
|
+
plans: auditPlan ? [auditPlan] : [],
|
|
5573
|
+
},
|
|
5574
|
+
...plan,
|
|
5575
|
+
};
|
|
5576
|
+
},
|
|
5577
|
+
map_next_override_assets: async (input) => {
|
|
5578
|
+
const projectRoot = normalizeOptionalString(input.projectRoot);
|
|
5579
|
+
if (!projectRoot) {
|
|
5580
|
+
throw new Error('projectRoot is required');
|
|
5581
|
+
}
|
|
5582
|
+
const sessionId = getSessionId(input);
|
|
5583
|
+
let observedAssets = input.observedAssets;
|
|
5584
|
+
let observedFromLiveTab;
|
|
5585
|
+
let observedFromPersisted;
|
|
5586
|
+
if (!Array.isArray(observedAssets) && sessionId) {
|
|
5587
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
5588
|
+
try {
|
|
5589
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_OBSERVE_ASSETS', { tabId, includePerformance: true }, 5_000);
|
|
5590
|
+
observedFromLiveTab = ensureCaptureSuccess(capture, sessionId);
|
|
5591
|
+
observedAssets = observedFromLiveTab.assets;
|
|
5592
|
+
if (getDb) {
|
|
5593
|
+
persistObservedOverrideAssets(getDb(), { ...observedFromLiveTab, sessionId, tabId: observedFromLiveTab.tabId ?? tabId });
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
catch (error) {
|
|
5597
|
+
if (!getDb || !isLiveSessionDisconnectedError(error)) {
|
|
5598
|
+
throw error;
|
|
5599
|
+
}
|
|
5600
|
+
observedAssets = listObservedOverrideAssets(getDb(), { sessionId });
|
|
5601
|
+
observedFromPersisted = { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 };
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
const mapping = await mapNextOverrideAssetsWithDrift({
|
|
5605
|
+
projectRoot,
|
|
5606
|
+
nextDir: normalizeOptionalString(input.nextDir),
|
|
5607
|
+
observedAssets,
|
|
5608
|
+
sourcePaths: input.sourcePaths,
|
|
5609
|
+
route: input.route,
|
|
5610
|
+
maxResults: input.maxResults,
|
|
5611
|
+
fetchProductionAssets: input.fetchProductionAssets,
|
|
5612
|
+
productionFetchTimeoutMs: input.productionFetchTimeoutMs,
|
|
5613
|
+
maxProductionAssetBytes: input.maxProductionAssetBytes,
|
|
5614
|
+
maxDriftCandidates: input.maxDriftCandidates,
|
|
5615
|
+
productionFetchConcurrency: input.productionFetchConcurrency,
|
|
5616
|
+
});
|
|
5617
|
+
return {
|
|
5618
|
+
...createBaseResponse(sessionId),
|
|
5619
|
+
limitsApplied: {
|
|
5620
|
+
maxResults: mapping.candidates.length,
|
|
5621
|
+
truncated: false,
|
|
5622
|
+
},
|
|
5623
|
+
observedFromLiveTab: observedFromLiveTab
|
|
5624
|
+
? {
|
|
5625
|
+
pageUrl: observedFromLiveTab.pageUrl,
|
|
5626
|
+
tabId: observedFromLiveTab.tabId,
|
|
5627
|
+
assetCount: Array.isArray(observedFromLiveTab.assets) ? observedFromLiveTab.assets.length : 0,
|
|
5628
|
+
}
|
|
5629
|
+
: undefined,
|
|
5630
|
+
observedFromPersisted,
|
|
5631
|
+
...mapping,
|
|
5632
|
+
};
|
|
5633
|
+
},
|
|
5634
|
+
plan_next_source_override: async (input) => {
|
|
5635
|
+
const projectRoot = normalizeOptionalString(input.projectRoot);
|
|
5636
|
+
if (!projectRoot) {
|
|
5637
|
+
throw new Error('projectRoot is required');
|
|
5638
|
+
}
|
|
5639
|
+
const sessionId = getSessionId(input);
|
|
5640
|
+
let observedAssets = input.observedAssets;
|
|
5641
|
+
let observedFromLiveTab;
|
|
5642
|
+
let observedFromPersisted;
|
|
5643
|
+
if (!Array.isArray(observedAssets) && sessionId) {
|
|
5644
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
5645
|
+
try {
|
|
5646
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_OBSERVE_ASSETS', { tabId, includePerformance: true }, 5_000);
|
|
5647
|
+
observedFromLiveTab = ensureCaptureSuccess(capture, sessionId);
|
|
5648
|
+
observedAssets = observedFromLiveTab.assets;
|
|
5649
|
+
if (getDb) {
|
|
5650
|
+
persistObservedOverrideAssets(getDb(), { ...observedFromLiveTab, sessionId, tabId: observedFromLiveTab.tabId ?? tabId });
|
|
5651
|
+
}
|
|
5652
|
+
}
|
|
5653
|
+
catch (error) {
|
|
5654
|
+
if (!getDb || !isLiveSessionDisconnectedError(error)) {
|
|
5655
|
+
throw error;
|
|
5656
|
+
}
|
|
5657
|
+
observedAssets = listObservedOverrideAssets(getDb(), { sessionId });
|
|
5658
|
+
observedFromPersisted = { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 };
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
const plan = await planNextSourceOverride({
|
|
5662
|
+
projectRoot,
|
|
5663
|
+
nextDir: normalizeOptionalString(input.nextDir),
|
|
5664
|
+
observedAssets,
|
|
5665
|
+
sourceEdits: input.sourceEdits,
|
|
5666
|
+
sourcePaths: input.sourcePaths,
|
|
5667
|
+
route: input.route,
|
|
5668
|
+
configPath: input.configPath,
|
|
5669
|
+
writeConfig: input.writeConfig,
|
|
5670
|
+
overwrite: input.overwrite,
|
|
5671
|
+
enabled: input.enabled,
|
|
5672
|
+
profileEnabled: input.profileEnabled,
|
|
5673
|
+
autoReload: input.autoReload,
|
|
5674
|
+
profileId: input.profileId,
|
|
5675
|
+
profileName: input.profileName,
|
|
5676
|
+
buildTimeoutMs: input.buildTimeoutMs,
|
|
5677
|
+
maxRules: input.maxRules,
|
|
5678
|
+
fetchProductionAssets: input.fetchProductionAssets,
|
|
5679
|
+
productionFetchTimeoutMs: input.productionFetchTimeoutMs,
|
|
5680
|
+
maxProductionAssetBytes: input.maxProductionAssetBytes,
|
|
5681
|
+
maxDriftCandidates: input.maxDriftCandidates,
|
|
5682
|
+
productionFetchConcurrency: input.productionFetchConcurrency,
|
|
5683
|
+
overlayTtlMs: input.overlayTtlMs,
|
|
5684
|
+
});
|
|
5685
|
+
const auditPlans = getDb
|
|
5686
|
+
? persistNextSourcePlanAudits({
|
|
5687
|
+
db: getDb(),
|
|
5688
|
+
sessionId,
|
|
5689
|
+
input,
|
|
5690
|
+
plan,
|
|
5691
|
+
})
|
|
5692
|
+
: [];
|
|
5693
|
+
return {
|
|
5694
|
+
...createBaseResponse(sessionId),
|
|
5695
|
+
limitsApplied: {
|
|
5696
|
+
maxResults: plan.rules.length,
|
|
5697
|
+
truncated: false,
|
|
5698
|
+
},
|
|
5699
|
+
observedFromLiveTab: observedFromLiveTab
|
|
5700
|
+
? {
|
|
5701
|
+
pageUrl: observedFromLiveTab.pageUrl,
|
|
5702
|
+
tabId: observedFromLiveTab.tabId,
|
|
5703
|
+
assetCount: Array.isArray(observedFromLiveTab.assets) ? observedFromLiveTab.assets.length : 0,
|
|
5704
|
+
}
|
|
5705
|
+
: undefined,
|
|
5706
|
+
observedFromPersisted,
|
|
5707
|
+
audit: {
|
|
5708
|
+
persisted: auditPlans.length > 0,
|
|
5709
|
+
plans: auditPlans,
|
|
5710
|
+
},
|
|
5711
|
+
...plan,
|
|
5712
|
+
};
|
|
5713
|
+
},
|
|
5714
|
+
get_override_status: async (input) => {
|
|
5715
|
+
const sessionId = getSessionId(input);
|
|
5716
|
+
if (!sessionId) {
|
|
5717
|
+
throw new Error('sessionId is required');
|
|
5718
|
+
}
|
|
5719
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_POC_GET_STATUS', {}, 3_000);
|
|
5720
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
5721
|
+
return {
|
|
5722
|
+
...createBaseResponse(sessionId),
|
|
5723
|
+
limitsApplied: {
|
|
5724
|
+
maxResults: 1,
|
|
5725
|
+
truncated: capture.truncated ?? false,
|
|
2200
5726
|
},
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
5727
|
+
preflight: getDb
|
|
5728
|
+
? buildOverridePreflight({
|
|
5729
|
+
db: getDb(),
|
|
5730
|
+
sessionId,
|
|
5731
|
+
profileId: input.profileId,
|
|
5732
|
+
getSessionConnectionState,
|
|
5733
|
+
})
|
|
5734
|
+
: null,
|
|
5735
|
+
...payload,
|
|
5736
|
+
nextActions: payload.lastErrorCode
|
|
5737
|
+
? [{ code: 'DIAGNOSE_OVERRIDES', message: 'Run diagnose_overrides for the latest override failure.' }]
|
|
5738
|
+
: payload.active === true
|
|
5739
|
+
? [{ code: 'GET_OVERRIDE_REQUEST_LOG', message: 'Inspect get_override_request_log after the target tab loads matching assets.' }]
|
|
5740
|
+
: [{ code: 'ENABLE_OVERRIDES', message: 'Enable overrides after validating the selected profile.' }],
|
|
2204
5741
|
};
|
|
2205
5742
|
},
|
|
2206
|
-
|
|
2207
|
-
const db = getDb();
|
|
5743
|
+
preflight_overrides: async (input) => {
|
|
2208
5744
|
const sessionId = getSessionId(input);
|
|
2209
5745
|
if (!sessionId) {
|
|
2210
5746
|
throw new Error('sessionId is required');
|
|
2211
5747
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
throw new Error('eventId is required');
|
|
5748
|
+
if (!getDb) {
|
|
5749
|
+
throw new Error('preflight_overrides requires database-backed override state');
|
|
2215
5750
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
5751
|
+
return {
|
|
5752
|
+
...createBaseResponse(sessionId),
|
|
5753
|
+
...buildOverridePreflight({
|
|
5754
|
+
db: getDb(),
|
|
5755
|
+
sessionId,
|
|
5756
|
+
profileId: input.profileId,
|
|
5757
|
+
getSessionConnectionState,
|
|
5758
|
+
}),
|
|
5759
|
+
};
|
|
5760
|
+
},
|
|
5761
|
+
enable_overrides: async (input) => {
|
|
5762
|
+
const sessionId = getSessionId(input);
|
|
5763
|
+
if (!sessionId) {
|
|
5764
|
+
throw new Error('sessionId is required');
|
|
2222
5765
|
}
|
|
2223
|
-
const
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
.
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
}
|
|
2240
|
-
|
|
2241
|
-
eventId: event.event_id,
|
|
2242
|
-
timestamp: event.ts,
|
|
2243
|
-
type: event.type,
|
|
2244
|
-
},
|
|
2245
|
-
matchReason: 'trigger_event_id',
|
|
2246
|
-
snapshot: mapSnapshotMetadata(byTriggerLink),
|
|
2247
|
-
};
|
|
5766
|
+
const preflight = getDb
|
|
5767
|
+
? buildOverridePreflight({
|
|
5768
|
+
db: getDb(),
|
|
5769
|
+
sessionId,
|
|
5770
|
+
profileId: input.profileId,
|
|
5771
|
+
getSessionConnectionState,
|
|
5772
|
+
})
|
|
5773
|
+
: null;
|
|
5774
|
+
if (preflight && preflight.ready !== true) {
|
|
5775
|
+
const blockingCodes = Array.isArray(preflight.issues)
|
|
5776
|
+
? preflight.issues
|
|
5777
|
+
.filter((issue) => isRecord(issue) && issue.severity === 'error')
|
|
5778
|
+
.map((issue) => String(issue.code ?? 'UNKNOWN'))
|
|
5779
|
+
: [];
|
|
5780
|
+
const profile = isRecord(preflight.profile) ? preflight.profile : {};
|
|
5781
|
+
if (!canBypassPreflightForExperimentalRsc(profile, blockingCodes)) {
|
|
5782
|
+
throw new Error(`Override preflight failed: ${blockingCodes.join(', ') || 'UNKNOWN'}`);
|
|
5783
|
+
}
|
|
2248
5784
|
}
|
|
2249
|
-
const
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
dom_json, styles_json, png_path, png_mime, png_bytes,
|
|
2253
|
-
dom_truncated, styles_truncated, png_truncated, created_at
|
|
2254
|
-
FROM snapshots
|
|
2255
|
-
WHERE session_id = ? AND ts BETWEEN ? AND ?
|
|
2256
|
-
ORDER BY ABS(ts - ?) ASC, ts ASC
|
|
2257
|
-
LIMIT 1`)
|
|
2258
|
-
.get(sessionId, event.ts, event.ts + maxDeltaMs, event.ts);
|
|
5785
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
5786
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_POC_ENABLE', { tabId }, 8_000);
|
|
5787
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
2259
5788
|
return {
|
|
2260
5789
|
...createBaseResponse(sessionId),
|
|
2261
5790
|
limitsApplied: {
|
|
2262
5791
|
maxResults: 1,
|
|
2263
|
-
truncated: false,
|
|
2264
|
-
},
|
|
2265
|
-
event: {
|
|
2266
|
-
eventId: event.event_id,
|
|
2267
|
-
timestamp: event.ts,
|
|
2268
|
-
type: event.type,
|
|
5792
|
+
truncated: capture.truncated ?? false,
|
|
2269
5793
|
},
|
|
2270
|
-
|
|
2271
|
-
|
|
5794
|
+
preflight,
|
|
5795
|
+
...payload,
|
|
5796
|
+
nextActions: [{ code: 'RELOAD_OR_INTERACT', message: 'Reload or interact with the tab so configured asset requests occur under the active override.' }],
|
|
2272
5797
|
};
|
|
2273
5798
|
},
|
|
2274
|
-
|
|
2275
|
-
const db = getDb();
|
|
5799
|
+
disable_overrides: async (input) => {
|
|
2276
5800
|
const sessionId = getSessionId(input);
|
|
2277
5801
|
if (!sessionId) {
|
|
2278
5802
|
throw new Error('sessionId is required');
|
|
2279
5803
|
}
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
throw new Error('snapshotId is required');
|
|
2283
|
-
}
|
|
2284
|
-
const assetType = input.asset === 'png' ? 'png' : 'png';
|
|
2285
|
-
const encoding = input.encoding === 'raw' ? 'raw' : 'base64';
|
|
2286
|
-
const offset = resolveOffset(input.offset);
|
|
2287
|
-
const maxBytes = resolveChunkBytes(input.maxBytes, DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES);
|
|
2288
|
-
const snapshot = db
|
|
2289
|
-
.prepare(`SELECT snapshot_id, session_id, png_path, png_mime, png_bytes
|
|
2290
|
-
FROM snapshots
|
|
2291
|
-
WHERE session_id = ? AND snapshot_id = ?
|
|
2292
|
-
LIMIT 1`)
|
|
2293
|
-
.get(sessionId, snapshotId);
|
|
2294
|
-
if (!snapshot) {
|
|
2295
|
-
throw new Error(`Snapshot not found: ${snapshotId}`);
|
|
2296
|
-
}
|
|
2297
|
-
if (assetType !== 'png' || !snapshot.png_path) {
|
|
2298
|
-
throw new Error('Requested snapshot asset is not available.');
|
|
2299
|
-
}
|
|
2300
|
-
const dbPath = getMainDbPath(db);
|
|
2301
|
-
const absolutePath = resolveSnapshotAbsolutePath(dbPath, snapshot.png_path);
|
|
2302
|
-
if (!existsSync(absolutePath)) {
|
|
2303
|
-
throw new Error(`Snapshot asset is missing on disk: ${snapshot.png_path}`);
|
|
2304
|
-
}
|
|
2305
|
-
const fullBuffer = readFileSync(absolutePath);
|
|
2306
|
-
if (offset >= fullBuffer.byteLength) {
|
|
2307
|
-
return {
|
|
2308
|
-
...createBaseResponse(sessionId),
|
|
2309
|
-
limitsApplied: {
|
|
2310
|
-
maxResults: maxBytes,
|
|
2311
|
-
truncated: false,
|
|
2312
|
-
},
|
|
2313
|
-
snapshotId,
|
|
2314
|
-
asset: assetType,
|
|
2315
|
-
assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
|
|
2316
|
-
mime: snapshot.png_mime ?? 'image/png',
|
|
2317
|
-
totalBytes: fullBuffer.byteLength,
|
|
2318
|
-
offset,
|
|
2319
|
-
returnedBytes: 0,
|
|
2320
|
-
hasMore: false,
|
|
2321
|
-
nextOffset: null,
|
|
2322
|
-
encoding,
|
|
2323
|
-
chunk: encoding === 'raw' ? [] : undefined,
|
|
2324
|
-
chunkBase64: encoding === 'base64' ? '' : undefined,
|
|
2325
|
-
};
|
|
2326
|
-
}
|
|
2327
|
-
const chunkBuffer = fullBuffer.subarray(offset, Math.min(offset + maxBytes, fullBuffer.byteLength));
|
|
2328
|
-
const returnedBytes = chunkBuffer.byteLength;
|
|
2329
|
-
const nextOffset = offset + returnedBytes;
|
|
2330
|
-
const hasMore = nextOffset < fullBuffer.byteLength;
|
|
5804
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_POC_DISABLE', {}, 5_000);
|
|
5805
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
2331
5806
|
return {
|
|
2332
5807
|
...createBaseResponse(sessionId),
|
|
2333
5808
|
limitsApplied: {
|
|
2334
|
-
maxResults:
|
|
2335
|
-
truncated:
|
|
5809
|
+
maxResults: 1,
|
|
5810
|
+
truncated: capture.truncated ?? false,
|
|
2336
5811
|
},
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
|
|
2340
|
-
mime: snapshot.png_mime ?? 'image/png',
|
|
2341
|
-
totalBytes: fullBuffer.byteLength,
|
|
2342
|
-
offset,
|
|
2343
|
-
returnedBytes,
|
|
2344
|
-
hasMore,
|
|
2345
|
-
nextOffset: hasMore ? nextOffset : null,
|
|
2346
|
-
encoding,
|
|
2347
|
-
chunk: encoding === 'raw' ? Array.from(chunkBuffer.values()) : undefined,
|
|
2348
|
-
chunkBase64: encoding === 'base64' ? chunkBuffer.toString('base64') : undefined,
|
|
5812
|
+
...payload,
|
|
5813
|
+
nextActions: [{ code: 'VERIFY_DISABLED', message: 'Run get_override_status if you need to confirm the debugger override is inactive.' }],
|
|
2349
5814
|
};
|
|
2350
5815
|
},
|
|
2351
|
-
};
|
|
2352
|
-
}
|
|
2353
|
-
export function createV2ToolHandlers(captureClient) {
|
|
2354
|
-
return {
|
|
2355
5816
|
get_dom_subtree: async (input) => {
|
|
2356
5817
|
const sessionId = getSessionId(input);
|
|
2357
5818
|
if (!sessionId) {
|
|
@@ -2445,6 +5906,345 @@ export function createV2ToolHandlers(captureClient) {
|
|
|
2445
5906
|
...ensureCaptureSuccess(capture, sessionId),
|
|
2446
5907
|
};
|
|
2447
5908
|
},
|
|
5909
|
+
get_page_state: async (input) => {
|
|
5910
|
+
const sessionId = getSessionId(input);
|
|
5911
|
+
if (!sessionId) {
|
|
5912
|
+
throw new Error('sessionId is required');
|
|
5913
|
+
}
|
|
5914
|
+
const capture = await capturePageState(sessionId, input);
|
|
5915
|
+
return {
|
|
5916
|
+
...createBaseResponse(sessionId),
|
|
5917
|
+
limitsApplied: capture.limitsApplied,
|
|
5918
|
+
...capture.payload,
|
|
5919
|
+
};
|
|
5920
|
+
},
|
|
5921
|
+
get_interactive_elements: async (input) => {
|
|
5922
|
+
const sessionId = getSessionId(input);
|
|
5923
|
+
if (!sessionId) {
|
|
5924
|
+
throw new Error('sessionId is required');
|
|
5925
|
+
}
|
|
5926
|
+
const kinds = resolveInteractiveKinds(input.kinds);
|
|
5927
|
+
const normalizedInput = {
|
|
5928
|
+
...input,
|
|
5929
|
+
includeButtons: kinds.includes('buttons'),
|
|
5930
|
+
includeInputs: kinds.includes('inputs'),
|
|
5931
|
+
includeModals: kinds.includes('modals'),
|
|
5932
|
+
};
|
|
5933
|
+
const capture = await capturePageState(sessionId, normalizedInput);
|
|
5934
|
+
const refs = collectInteractiveElementRefs(capture.payload, kinds, capture.limitsApplied.maxResults);
|
|
5935
|
+
return {
|
|
5936
|
+
...createBaseResponse(sessionId),
|
|
5937
|
+
limitsApplied: {
|
|
5938
|
+
maxResults: capture.limitsApplied.maxResults,
|
|
5939
|
+
truncated: capture.limitsApplied.truncated || refs.length >= capture.limitsApplied.maxResults,
|
|
5940
|
+
},
|
|
5941
|
+
kinds,
|
|
5942
|
+
refs,
|
|
5943
|
+
page: {
|
|
5944
|
+
url: capture.payload.url,
|
|
5945
|
+
title: capture.payload.title,
|
|
5946
|
+
language: capture.payload.language,
|
|
5947
|
+
viewport: capture.payload.viewport,
|
|
5948
|
+
},
|
|
5949
|
+
pageSummary: typeof capture.payload.summary === 'object' && capture.payload.summary !== null
|
|
5950
|
+
? capture.payload.summary
|
|
5951
|
+
: undefined,
|
|
5952
|
+
};
|
|
5953
|
+
},
|
|
5954
|
+
set_viewport: async (input) => {
|
|
5955
|
+
const sessionId = getSessionId(input);
|
|
5956
|
+
if (!sessionId) {
|
|
5957
|
+
throw new Error('sessionId is required');
|
|
5958
|
+
}
|
|
5959
|
+
const width = resolveViewportDimension(input.width, 'width');
|
|
5960
|
+
const height = resolveViewportDimension(input.height, 'height');
|
|
5961
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'SET_VIEWPORT', {
|
|
5962
|
+
width,
|
|
5963
|
+
height,
|
|
5964
|
+
}, 5_000);
|
|
5965
|
+
return {
|
|
5966
|
+
...createBaseResponse(sessionId),
|
|
5967
|
+
limitsApplied: {
|
|
5968
|
+
maxResults: 1,
|
|
5969
|
+
truncated: capture.truncated ?? false,
|
|
5970
|
+
},
|
|
5971
|
+
...ensureCaptureSuccess(capture, sessionId),
|
|
5972
|
+
};
|
|
5973
|
+
},
|
|
5974
|
+
assert_page_state: async (input) => {
|
|
5975
|
+
const sessionId = getSessionId(input);
|
|
5976
|
+
if (!sessionId) {
|
|
5977
|
+
throw new Error('sessionId is required');
|
|
5978
|
+
}
|
|
5979
|
+
const matcher = resolvePageStateMatcher(input);
|
|
5980
|
+
const capture = await capturePageState(sessionId, input);
|
|
5981
|
+
const assertion = evaluatePageStateAssertion(capture.payload, matcher);
|
|
5982
|
+
return {
|
|
5983
|
+
...createBaseResponse(sessionId),
|
|
5984
|
+
limitsApplied: capture.limitsApplied,
|
|
5985
|
+
matcher,
|
|
5986
|
+
matched: assertion.matched,
|
|
5987
|
+
matchCount: assertion.matchCount,
|
|
5988
|
+
expectedCount: assertion.expectedCount,
|
|
5989
|
+
sampledMatches: assertion.sampledMatches,
|
|
5990
|
+
pageSummary: assertion.summary,
|
|
5991
|
+
page: {
|
|
5992
|
+
url: capture.payload.url,
|
|
5993
|
+
title: capture.payload.title,
|
|
5994
|
+
language: capture.payload.language,
|
|
5995
|
+
viewport: capture.payload.viewport,
|
|
5996
|
+
},
|
|
5997
|
+
};
|
|
5998
|
+
},
|
|
5999
|
+
wait_for_page_state: async (input) => {
|
|
6000
|
+
const sessionId = getSessionId(input);
|
|
6001
|
+
if (!sessionId) {
|
|
6002
|
+
throw new Error('sessionId is required');
|
|
6003
|
+
}
|
|
6004
|
+
const waited = await waitForPageStateCondition(sessionId, input, capturePageState);
|
|
6005
|
+
return {
|
|
6006
|
+
...createBaseResponse(sessionId),
|
|
6007
|
+
...waited,
|
|
6008
|
+
};
|
|
6009
|
+
},
|
|
6010
|
+
run_ui_steps: async (input) => {
|
|
6011
|
+
const request = RunUIStepsSchema.parse(input);
|
|
6012
|
+
const workflowTraceId = createUIWorkflowTraceId();
|
|
6013
|
+
const workflowStartedAt = Date.now();
|
|
6014
|
+
const stepResults = [];
|
|
6015
|
+
let lastPageCapture;
|
|
6016
|
+
let failedStepId;
|
|
6017
|
+
let stoppedAtIndex = request.steps.length;
|
|
6018
|
+
let stateCaptureCount = 0;
|
|
6019
|
+
let failureCaptureCount = 0;
|
|
6020
|
+
let retryCount = 0;
|
|
6021
|
+
const workflowCapturePageState = async (sessionId, toolInput) => {
|
|
6022
|
+
stateCaptureCount += 1;
|
|
6023
|
+
return capturePageState(sessionId, toolInput);
|
|
6024
|
+
};
|
|
6025
|
+
for (const [index, step] of request.steps.entries()) {
|
|
6026
|
+
const stepId = createWorkflowStepId(step, index);
|
|
6027
|
+
const failurePolicy = resolveWorkflowFailurePolicy(step, request.stopOnFailure);
|
|
6028
|
+
let executionAttempts = 0;
|
|
6029
|
+
let finalStepResult;
|
|
6030
|
+
let stepFailed = false;
|
|
6031
|
+
while (true) {
|
|
6032
|
+
executionAttempts += 1;
|
|
6033
|
+
const startedAt = Date.now();
|
|
6034
|
+
const previousCapture = lastPageCapture;
|
|
6035
|
+
try {
|
|
6036
|
+
if (step.kind === 'action') {
|
|
6037
|
+
const resolvedTarget = await resolveWorkflowActionTarget(request.sessionId, step.target, workflowCapturePageState, request.mode === 'fast' ? lastPageCapture : undefined);
|
|
6038
|
+
const liveRequest = LiveUIActionRequestSchema.parse({
|
|
6039
|
+
action: step.action,
|
|
6040
|
+
target: resolvedTarget.target,
|
|
6041
|
+
traceId: step.traceId ?? `${workflowTraceId}:${stepId}`,
|
|
6042
|
+
...(step.input ? { input: step.input } : {}),
|
|
6043
|
+
});
|
|
6044
|
+
const capture = await executeLiveCapture(captureClient, request.sessionId, 'EXECUTE_UI_ACTION', liveRequest, 5_000);
|
|
6045
|
+
const payload = ensureCaptureSuccess(capture, request.sessionId);
|
|
6046
|
+
const actionResult = payload;
|
|
6047
|
+
const failed = actionResult.status === 'failed' || actionResult.status === 'rejected';
|
|
6048
|
+
let currentCapture = resolvedTarget.pageCapture ?? lastPageCapture;
|
|
6049
|
+
if (!failed && request.mode === 'fast') {
|
|
6050
|
+
await sleep(75);
|
|
6051
|
+
currentCapture = await captureWorkflowPageState(request.sessionId, workflowCapturePageState, request.mode);
|
|
6052
|
+
}
|
|
6053
|
+
lastPageCapture = currentCapture;
|
|
6054
|
+
finalStepResult = {
|
|
6055
|
+
id: stepId,
|
|
6056
|
+
kind: step.kind,
|
|
6057
|
+
status: failed ? 'failed' : 'succeeded',
|
|
6058
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
6059
|
+
action: step.action,
|
|
6060
|
+
traceId: actionResult.traceId,
|
|
6061
|
+
target: {
|
|
6062
|
+
resolution: resolvedTarget.resolution,
|
|
6063
|
+
actionTarget: typeof actionResult.target === 'object' && actionResult.target !== null
|
|
6064
|
+
? actionResult.target
|
|
6065
|
+
: undefined,
|
|
6066
|
+
},
|
|
6067
|
+
error: failed && actionResult.failureReason
|
|
6068
|
+
? {
|
|
6069
|
+
code: actionResult.failureReason.code,
|
|
6070
|
+
message: actionResult.failureReason.message,
|
|
6071
|
+
}
|
|
6072
|
+
: undefined,
|
|
6073
|
+
pageChangeSummary: createPageChangeSummary(previousCapture, currentCapture),
|
|
6074
|
+
};
|
|
6075
|
+
}
|
|
6076
|
+
else if (step.kind === 'waitFor') {
|
|
6077
|
+
const waitInput = {
|
|
6078
|
+
...step.matcher,
|
|
6079
|
+
timeoutMs: step.matcher.timeoutMs ?? request.defaultTimeoutMs,
|
|
6080
|
+
pollIntervalMs: step.matcher.pollIntervalMs ?? request.defaultPollIntervalMs,
|
|
6081
|
+
};
|
|
6082
|
+
const waited = await waitForPageStateConditionDetailed(request.sessionId, waitInput, workflowCapturePageState, request.mode === 'fast' ? lastPageCapture : undefined);
|
|
6083
|
+
lastPageCapture = waited.lastCapture ?? lastPageCapture;
|
|
6084
|
+
finalStepResult = {
|
|
6085
|
+
id: stepId,
|
|
6086
|
+
kind: step.kind,
|
|
6087
|
+
status: waited.matched ? 'succeeded' : 'failed',
|
|
6088
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
6089
|
+
matcher: waited.matcher,
|
|
6090
|
+
matchCount: waited.matchCount,
|
|
6091
|
+
waitedMs: waited.waitedMs,
|
|
6092
|
+
attempts: waited.attempts,
|
|
6093
|
+
error: waited.matched
|
|
6094
|
+
? undefined
|
|
6095
|
+
: {
|
|
6096
|
+
code: 'page_state_not_matched',
|
|
6097
|
+
message: 'Workflow wait step timed out before the requested page state appeared.',
|
|
6098
|
+
},
|
|
6099
|
+
pageChangeSummary: createPageChangeSummary(previousCapture, waited.lastCapture),
|
|
6100
|
+
};
|
|
6101
|
+
}
|
|
6102
|
+
else {
|
|
6103
|
+
const capture = request.mode === 'fast' && lastPageCapture
|
|
6104
|
+
? lastPageCapture
|
|
6105
|
+
: await workflowCapturePageState(request.sessionId, step.matcher);
|
|
6106
|
+
const assertion = evaluatePageStateAssertion(capture.payload, resolvePageStateMatcher(step.matcher));
|
|
6107
|
+
lastPageCapture = capture;
|
|
6108
|
+
finalStepResult = {
|
|
6109
|
+
id: stepId,
|
|
6110
|
+
kind: step.kind,
|
|
6111
|
+
status: assertion.matched ? 'succeeded' : 'failed',
|
|
6112
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
6113
|
+
matcher: step.matcher,
|
|
6114
|
+
matchCount: assertion.matchCount,
|
|
6115
|
+
error: assertion.matched
|
|
6116
|
+
? undefined
|
|
6117
|
+
: {
|
|
6118
|
+
code: 'page_state_assertion_failed',
|
|
6119
|
+
message: 'Workflow assert step did not match the requested page state.',
|
|
6120
|
+
},
|
|
6121
|
+
pageChangeSummary: createPageChangeSummary(previousCapture, capture),
|
|
6122
|
+
};
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
6125
|
+
catch (error) {
|
|
6126
|
+
const workflowError = error instanceof WorkflowTargetResolutionError ? error : undefined;
|
|
6127
|
+
finalStepResult = {
|
|
6128
|
+
id: stepId,
|
|
6129
|
+
kind: step.kind,
|
|
6130
|
+
status: 'failed',
|
|
6131
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
6132
|
+
action: step.kind === 'action' ? step.action : undefined,
|
|
6133
|
+
target: step.kind === 'action' && workflowError
|
|
6134
|
+
? workflowError.details
|
|
6135
|
+
: undefined,
|
|
6136
|
+
matcher: step.kind === 'action' ? undefined : step.matcher,
|
|
6137
|
+
error: normalizeWorkflowError(error),
|
|
6138
|
+
};
|
|
6139
|
+
}
|
|
6140
|
+
stepFailed = finalStepResult.status === 'failed';
|
|
6141
|
+
if (stepFailed && failurePolicy.strategy === 'retry_once' && executionAttempts === 1) {
|
|
6142
|
+
retryCount += 1;
|
|
6143
|
+
await sleep(100);
|
|
6144
|
+
continue;
|
|
6145
|
+
}
|
|
6146
|
+
break;
|
|
6147
|
+
}
|
|
6148
|
+
finalStepResult.executionAttempts = executionAttempts;
|
|
6149
|
+
finalStepResult.failurePolicy = {
|
|
6150
|
+
strategy: failurePolicy.strategy,
|
|
6151
|
+
captureEnabled: Boolean(failurePolicy.captureOptions?.enabled),
|
|
6152
|
+
};
|
|
6153
|
+
finalStepResult.recommendedAction = resolveWorkflowRecommendedAction(finalStepResult.error);
|
|
6154
|
+
if (stepFailed && failurePolicy.captureOptions) {
|
|
6155
|
+
const evidence = await captureFailureSnapshot(captureClient, request.sessionId, resolveWorkflowFailureSelector(step, finalStepResult.target), failurePolicy.captureOptions);
|
|
6156
|
+
if (evidence) {
|
|
6157
|
+
failureCaptureCount += 1;
|
|
6158
|
+
finalStepResult.failureEvidence = evidence;
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
stepResults.push(finalStepResult);
|
|
6162
|
+
if (stepFailed) {
|
|
6163
|
+
failedStepId ??= stepId;
|
|
6164
|
+
if (failurePolicy.strategy !== 'continue') {
|
|
6165
|
+
stoppedAtIndex = index + 1;
|
|
6166
|
+
break;
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
}
|
|
6170
|
+
if (failedStepId && stoppedAtIndex < request.steps.length) {
|
|
6171
|
+
for (const [index, step] of request.steps.slice(stoppedAtIndex).entries()) {
|
|
6172
|
+
stepResults.push({
|
|
6173
|
+
id: createWorkflowStepId(step, stoppedAtIndex + index),
|
|
6174
|
+
kind: step.kind,
|
|
6175
|
+
status: 'skipped',
|
|
6176
|
+
durationMs: 0,
|
|
6177
|
+
action: step.kind === 'action' ? step.action : undefined,
|
|
6178
|
+
matcher: step.kind === 'action' ? undefined : step.matcher,
|
|
6179
|
+
pageChangeSummary: undefined,
|
|
6180
|
+
error: {
|
|
6181
|
+
code: 'workflow_stopped_early',
|
|
6182
|
+
message: `Skipped because workflow stopped after failed step "${failedStepId}".`,
|
|
6183
|
+
},
|
|
6184
|
+
});
|
|
6185
|
+
}
|
|
6186
|
+
}
|
|
6187
|
+
let finalPageSummary;
|
|
6188
|
+
let finalPage;
|
|
6189
|
+
let finalCaptureTruncated = false;
|
|
6190
|
+
try {
|
|
6191
|
+
const finalCapture = lastPageCapture ?? await captureWorkflowPageState(request.sessionId, workflowCapturePageState, request.mode);
|
|
6192
|
+
finalPageSummary =
|
|
6193
|
+
typeof finalCapture.payload.summary === 'object' && finalCapture.payload.summary !== null
|
|
6194
|
+
? finalCapture.payload.summary
|
|
6195
|
+
: undefined;
|
|
6196
|
+
finalPage = {
|
|
6197
|
+
url: finalCapture.payload.url,
|
|
6198
|
+
title: finalCapture.payload.title,
|
|
6199
|
+
language: finalCapture.payload.language,
|
|
6200
|
+
viewport: finalCapture.payload.viewport,
|
|
6201
|
+
};
|
|
6202
|
+
finalCaptureTruncated = finalCapture.limitsApplied.truncated;
|
|
6203
|
+
}
|
|
6204
|
+
catch {
|
|
6205
|
+
finalPageSummary = undefined;
|
|
6206
|
+
finalPage = undefined;
|
|
6207
|
+
}
|
|
6208
|
+
const workflowFinishedAt = Date.now();
|
|
6209
|
+
const succeededSteps = stepResults.filter((step) => step.status === 'succeeded').length;
|
|
6210
|
+
const failedSteps = stepResults.filter((step) => step.status === 'failed').length;
|
|
6211
|
+
const skippedSteps = stepResults.filter((step) => step.status === 'skipped').length;
|
|
6212
|
+
const failedStep = failedStepId
|
|
6213
|
+
? stepResults.find((step) => step.id === failedStepId && step.status === 'failed')
|
|
6214
|
+
: undefined;
|
|
6215
|
+
return {
|
|
6216
|
+
...createBaseResponse(request.sessionId),
|
|
6217
|
+
limitsApplied: {
|
|
6218
|
+
maxResults: request.steps.length,
|
|
6219
|
+
truncated: finalCaptureTruncated,
|
|
6220
|
+
},
|
|
6221
|
+
traceId: workflowTraceId,
|
|
6222
|
+
mode: request.mode,
|
|
6223
|
+
status: failedStepId ? 'failed' : 'succeeded',
|
|
6224
|
+
startedAt: workflowStartedAt,
|
|
6225
|
+
finishedAt: workflowFinishedAt,
|
|
6226
|
+
durationMs: Math.max(0, workflowFinishedAt - workflowStartedAt),
|
|
6227
|
+
requestedStepCount: request.steps.length,
|
|
6228
|
+
completedStepCount: succeededSteps,
|
|
6229
|
+
failedStepId,
|
|
6230
|
+
stoppedEarly: Boolean(failedStepId && stoppedAtIndex < request.steps.length),
|
|
6231
|
+
recommendedAction: failedStep?.recommendedAction,
|
|
6232
|
+
stepCounts: {
|
|
6233
|
+
succeeded: succeededSteps,
|
|
6234
|
+
failed: failedSteps,
|
|
6235
|
+
skipped: skippedSteps,
|
|
6236
|
+
},
|
|
6237
|
+
workflowDiagnostics: {
|
|
6238
|
+
retryCount,
|
|
6239
|
+
stateCaptureCount,
|
|
6240
|
+
failureCaptureCount,
|
|
6241
|
+
usedCachedState: request.mode === 'fast',
|
|
6242
|
+
},
|
|
6243
|
+
steps: stepResults,
|
|
6244
|
+
finalPageSummary,
|
|
6245
|
+
finalPage,
|
|
6246
|
+
};
|
|
6247
|
+
},
|
|
2448
6248
|
capture_ui_snapshot: async (input) => {
|
|
2449
6249
|
const sessionId = getSessionId(input);
|
|
2450
6250
|
if (!sessionId) {
|
|
@@ -2482,21 +6282,11 @@ export function createV2ToolHandlers(captureClient) {
|
|
|
2482
6282
|
llmRequested: true,
|
|
2483
6283
|
}, 5_000);
|
|
2484
6284
|
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
2485
|
-
const snapshotRecord =
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
delete snapshotObject.dom;
|
|
2491
|
-
}
|
|
2492
|
-
if (!includeStyles) {
|
|
2493
|
-
delete snapshotObject.styles;
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
const png = snapshotRecord.png;
|
|
2497
|
-
if (!includePngDataUrl && typeof png === 'object' && png !== null) {
|
|
2498
|
-
delete png.dataUrl;
|
|
2499
|
-
}
|
|
6285
|
+
const snapshotRecord = normalizeSnapshotResponsePayload(payload, {
|
|
6286
|
+
includeDom,
|
|
6287
|
+
includeStyles,
|
|
6288
|
+
includePngDataUrl,
|
|
6289
|
+
});
|
|
2500
6290
|
return {
|
|
2501
6291
|
...createBaseResponse(sessionId),
|
|
2502
6292
|
limitsApplied: {
|
|
@@ -2577,6 +6367,72 @@ export function createV2ToolHandlers(captureClient) {
|
|
|
2577
6367
|
bufferStats: payload.bufferStats,
|
|
2578
6368
|
};
|
|
2579
6369
|
},
|
|
6370
|
+
execute_ui_action: async (input) => {
|
|
6371
|
+
const sessionId = getSessionId(input);
|
|
6372
|
+
if (!sessionId) {
|
|
6373
|
+
throw new Error('sessionId is required');
|
|
6374
|
+
}
|
|
6375
|
+
const actionInput = { ...input };
|
|
6376
|
+
delete actionInput.sessionId;
|
|
6377
|
+
delete actionInput.captureOnFailure;
|
|
6378
|
+
const request = LiveUIActionRequestSchema.parse(actionInput);
|
|
6379
|
+
const failureCaptureOptions = resolveFailureEvidenceCaptureOptions(input);
|
|
6380
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'EXECUTE_UI_ACTION', request, 5_000);
|
|
6381
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
6382
|
+
const actionResult = payload;
|
|
6383
|
+
const failed = actionResult.status === 'failed' || actionResult.status === 'rejected';
|
|
6384
|
+
const failureEvidence = failed
|
|
6385
|
+
? await captureFailureEvidence(captureClient, sessionId, request, failureCaptureOptions)
|
|
6386
|
+
: undefined;
|
|
6387
|
+
const postActionWaitInput = typeof input.waitForPageState === 'object' && input.waitForPageState !== null
|
|
6388
|
+
? {
|
|
6389
|
+
...input.waitForPageState,
|
|
6390
|
+
}
|
|
6391
|
+
: undefined;
|
|
6392
|
+
const postActionState = actionResult.status === 'succeeded' && postActionWaitInput
|
|
6393
|
+
? await waitForPageStateCondition(sessionId, postActionWaitInput, capturePageState)
|
|
6394
|
+
: undefined;
|
|
6395
|
+
const evidenceTruncated = Boolean(failureEvidence
|
|
6396
|
+
&& typeof failureEvidence === 'object'
|
|
6397
|
+
&& failureEvidence !== null
|
|
6398
|
+
&& typeof failureEvidence.limitsApplied?.truncated === 'boolean'
|
|
6399
|
+
&& failureEvidence.limitsApplied.truncated);
|
|
6400
|
+
const target = typeof actionResult.target === 'object' && actionResult.target !== null
|
|
6401
|
+
? actionResult.target
|
|
6402
|
+
: {};
|
|
6403
|
+
return {
|
|
6404
|
+
...createBaseResponse(sessionId),
|
|
6405
|
+
limitsApplied: {
|
|
6406
|
+
maxResults: 1,
|
|
6407
|
+
truncated: (capture.truncated ?? false)
|
|
6408
|
+
|| evidenceTruncated
|
|
6409
|
+
|| Boolean(postActionState?.limitsApplied.truncated),
|
|
6410
|
+
},
|
|
6411
|
+
action: actionResult.action,
|
|
6412
|
+
status: actionResult.status,
|
|
6413
|
+
traceId: actionResult.traceId,
|
|
6414
|
+
startedAt: actionResult.startedAt,
|
|
6415
|
+
finishedAt: actionResult.finishedAt,
|
|
6416
|
+
durationMs: typeof actionResult.startedAt === 'number' && typeof actionResult.finishedAt === 'number'
|
|
6417
|
+
? Math.max(0, actionResult.finishedAt - actionResult.startedAt)
|
|
6418
|
+
: undefined,
|
|
6419
|
+
actionResult,
|
|
6420
|
+
target,
|
|
6421
|
+
tabContext: {
|
|
6422
|
+
tabId: typeof target.tabId === 'number' ? target.tabId : undefined,
|
|
6423
|
+
frameId: typeof target.frameId === 'number' ? target.frameId : 0,
|
|
6424
|
+
url: typeof target.url === 'string' ? target.url : undefined,
|
|
6425
|
+
},
|
|
6426
|
+
failureDetails: actionResult.failureReason,
|
|
6427
|
+
postActionEvidence: failureEvidence,
|
|
6428
|
+
postActionState,
|
|
6429
|
+
supportedScopes: {
|
|
6430
|
+
executionScope: actionResult.executionScope,
|
|
6431
|
+
topDocumentOnly: true,
|
|
6432
|
+
opensNewBrowserSession: false,
|
|
6433
|
+
},
|
|
6434
|
+
};
|
|
6435
|
+
},
|
|
2580
6436
|
};
|
|
2581
6437
|
}
|
|
2582
6438
|
function isRecord(value) {
|
|
@@ -2634,7 +6490,9 @@ export async function routeToolCall(tools, toolName, input) {
|
|
|
2634
6490
|
}
|
|
2635
6491
|
export function createMCPServer(overrides = {}, options = {}) {
|
|
2636
6492
|
const logger = options.logger ?? createDefaultMcpLogger();
|
|
2637
|
-
const v2Handlers = options.captureClient
|
|
6493
|
+
const v2Handlers = options.captureClient
|
|
6494
|
+
? createV2ToolHandlers(options.captureClient, () => getConnection().db, options.getSessionConnectionState)
|
|
6495
|
+
: {};
|
|
2638
6496
|
const tools = createToolRegistry({
|
|
2639
6497
|
...createV1ToolHandlers(() => getConnection().db, options.getSessionConnectionState),
|
|
2640
6498
|
...v2Handlers,
|