@wenathlan/extension 1.1.52 → 1.1.54
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 +7 -5
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2106 -596
- package/dist/index.js.map +4 -4
- package/dist/mcpserver.d.ts +167 -0
- package/dist/mcpserver.d.ts.map +1 -0
- package/dist/memory.d.ts +74 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +48 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +102 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/toolcatalog.d.ts +31 -0
- package/dist/toolcatalog.d.ts.map +1 -0
- package/dist/types.d.ts +355 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/workflow.d.ts +19 -3
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workfloweditor.d.ts +108 -0
- package/dist/workfloweditor.d.ts.map +1 -0
- package/extension/dist/background.js +1773 -20
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +1 -1
- package/extension/dist/pagebridge.js.map +2 -2
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +12 -1
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +1419 -1
- package/extension/dist/sidepanel.js.map +4 -4
- package/extension/dist/style.css +2 -0
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -2282,6 +2282,162 @@ var sessionmemory = class {
|
|
|
2282
2282
|
async listmanualruns() {
|
|
2283
2283
|
return await this.adapter.get("manualruns") ?? [];
|
|
2284
2284
|
}
|
|
2285
|
+
/** Stores one workflow version record with its change note; saving the same version again replaces its note while older versions survive for the timeline. */
|
|
2286
|
+
async addworkflowversion(version) {
|
|
2287
|
+
const versions = (await this.listworkflowversions()).filter((entry) => !(entry.workflowid === version.workflowid && entry.version === version.version));
|
|
2288
|
+
await this.adapter.set("workflowversions", [version, ...versions]);
|
|
2289
|
+
}
|
|
2290
|
+
/** Returns every stored workflow version record, newest first, optionally filtered to one workflow. */
|
|
2291
|
+
async listworkflowversions(workflowid) {
|
|
2292
|
+
const versions = await this.adapter.get("workflowversions") ?? [];
|
|
2293
|
+
return workflowid === void 0 ? versions : versions.filter((entry) => entry.workflowid === workflowid);
|
|
2294
|
+
}
|
|
2295
|
+
/** Stores one version diff result for the history view. */
|
|
2296
|
+
async addversiondiff(diff) {
|
|
2297
|
+
const diffs = (await this.listversiondiffs()).filter((entry) => !(entry.workflowid === diff.workflowid && entry.from === diff.from && entry.to === diff.to));
|
|
2298
|
+
await this.adapter.set("versiondiffs", [diff, ...diffs]);
|
|
2299
|
+
}
|
|
2300
|
+
/** Returns every stored version diff result, newest first, optionally filtered to one workflow. */
|
|
2301
|
+
async listversiondiffs(workflowid) {
|
|
2302
|
+
const diffs = await this.adapter.get("versiondiffs") ?? [];
|
|
2303
|
+
return workflowid === void 0 ? diffs : diffs.filter((entry) => entry.workflowid === workflowid);
|
|
2304
|
+
}
|
|
2305
|
+
/** Records one run history entry — the outcome, duration and trigger cause of one execution — under the user configured retention window with no code ceiling. */
|
|
2306
|
+
async addrunhistory(entry) {
|
|
2307
|
+
const entries = await this.gethistory();
|
|
2308
|
+
const combined = [entry, ...entries];
|
|
2309
|
+
const retention = (await this.getsettings())?.runhistoryretention;
|
|
2310
|
+
await this.adapter.set("runhistory", retention === void 0 ? combined : combined.slice(0, retention));
|
|
2311
|
+
}
|
|
2312
|
+
/** Returns the stored run history, newest first, filtered by workflow, outcome and time floor; the filters stay user choices. */
|
|
2313
|
+
async gethistory(filter) {
|
|
2314
|
+
const entries = await this.adapter.get("runhistory") ?? [];
|
|
2315
|
+
let filtered = entries;
|
|
2316
|
+
if (filter?.workflowid !== void 0) filtered = filtered.filter((entry) => entry.workflowid === filter.workflowid);
|
|
2317
|
+
if (filter?.outcome !== void 0) filtered = filtered.filter((entry) => entry.outcome === filter.outcome);
|
|
2318
|
+
if (filter?.since !== void 0) filtered = filtered.filter((entry) => entry.endedat >= filter.since);
|
|
2319
|
+
if (filter?.limit !== void 0) filtered = filtered.slice(0, filter.limit);
|
|
2320
|
+
return filtered;
|
|
2321
|
+
}
|
|
2322
|
+
/** Stores the editor layout of one workflow so the canvas reopens exactly as left. */
|
|
2323
|
+
async seteditorlayout(workflowid, layout) {
|
|
2324
|
+
return this.adapter.set(`editorlayout${workflowid}`, layout);
|
|
2325
|
+
}
|
|
2326
|
+
/** Returns the stored editor layout of one workflow. */
|
|
2327
|
+
async geteditorlayout(workflowid) {
|
|
2328
|
+
return await this.adapter.get(`editorlayout${workflowid}`) ?? void 0;
|
|
2329
|
+
}
|
|
2330
|
+
/** Stores the breakpoint step ids of one workflow. */
|
|
2331
|
+
async setworkflowbreakpoints(workflowid, stepids) {
|
|
2332
|
+
return this.adapter.set(`workflowbreakpoints${workflowid}`, stepids);
|
|
2333
|
+
}
|
|
2334
|
+
/** Returns the stored breakpoint step ids of one workflow, oldest first. */
|
|
2335
|
+
async getworkflowbreakpoints(workflowid) {
|
|
2336
|
+
return await this.adapter.get(`workflowbreakpoints${workflowid}`) ?? [];
|
|
2337
|
+
}
|
|
2338
|
+
/** Stores one per site policy override; re-adding the same id replaces its deltas. */
|
|
2339
|
+
async addsiteoverride(override) {
|
|
2340
|
+
const overrides = (await this.listsiteoverrides()).filter((entry) => entry.id !== override.id);
|
|
2341
|
+
await this.adapter.set("siteoverrides", [override, ...overrides]);
|
|
2342
|
+
}
|
|
2343
|
+
/** Returns every stored per site override, newest first, optionally filtered to one workflow. */
|
|
2344
|
+
async listsiteoverrides(workflowid) {
|
|
2345
|
+
const overrides = await this.adapter.get("siteoverrides") ?? [];
|
|
2346
|
+
return workflowid === void 0 ? overrides : overrides.filter((entry) => entry.workflowid === workflowid);
|
|
2347
|
+
}
|
|
2348
|
+
/** Removes one per site override when the user deletes it. */
|
|
2349
|
+
async removesiteoverride(id) {
|
|
2350
|
+
await this.adapter.set("siteoverrides", (await this.listsiteoverrides()).filter((entry) => entry.id !== id));
|
|
2351
|
+
}
|
|
2352
|
+
/** Stores one watchdog event with its recovery outcome; the event history keeps the audit trail of every scan. */
|
|
2353
|
+
async addwatchdogevent(event) {
|
|
2354
|
+
const events = (await this.listwatchdogevents()).filter((entry) => entry.id !== event.id);
|
|
2355
|
+
await this.adapter.set("watchdogevents", [event, ...events]);
|
|
2356
|
+
}
|
|
2357
|
+
/** Returns every stored watchdog event, newest first. */
|
|
2358
|
+
async listwatchdogevents() {
|
|
2359
|
+
return await this.adapter.get("watchdogevents") ?? [];
|
|
2360
|
+
}
|
|
2361
|
+
/** Stores one pending workflow import held for review; approving it later stores the record as runnable. */
|
|
2362
|
+
async addworkflowimport(entry) {
|
|
2363
|
+
const imports = (await this.listworkflowimports()).filter((candidate) => candidate.id !== entry.id);
|
|
2364
|
+
await this.adapter.set("workflowimports", [entry, ...imports]);
|
|
2365
|
+
}
|
|
2366
|
+
/** Returns every pending workflow import, newest first. */
|
|
2367
|
+
async listworkflowimports() {
|
|
2368
|
+
return await this.adapter.get("workflowimports") ?? [];
|
|
2369
|
+
}
|
|
2370
|
+
/** Removes one pending import when the user approves or rejects it. */
|
|
2371
|
+
async removeworkflowimport(id) {
|
|
2372
|
+
await this.adapter.set("workflowimports", (await this.listworkflowimports()).filter((entry) => entry.id !== id));
|
|
2373
|
+
}
|
|
2374
|
+
/** Stores the per workflow background run flags so a workflow keeps running with the panel closed. */
|
|
2375
|
+
async setbackgroundruns(flags) {
|
|
2376
|
+
return this.adapter.set("backgroundruns", flags);
|
|
2377
|
+
}
|
|
2378
|
+
/** Returns the per workflow background run flags. */
|
|
2379
|
+
async getbackgroundruns() {
|
|
2380
|
+
return await this.adapter.get("backgroundruns") ?? {};
|
|
2381
|
+
}
|
|
2382
|
+
/** Removes one stored workflow record version; a rejected import or rollback disappears from the library while every other version survives. */
|
|
2383
|
+
async removeworkflowversion(id, version) {
|
|
2384
|
+
await this.adapter.set("workflowrecords", (await this.getworkflowrecordversions()).filter((entry) => !(entry.id === id && entry.version === version)));
|
|
2385
|
+
}
|
|
2386
|
+
/** Returns every stored mcp client record, newest first. */
|
|
2387
|
+
async getclients() {
|
|
2388
|
+
return await this.adapter.get("mcpclients") ?? [];
|
|
2389
|
+
}
|
|
2390
|
+
/** Upserts one mcp client record by its id so one clientrecord stays per connected transport. */
|
|
2391
|
+
async setclient(client) {
|
|
2392
|
+
const records = (await this.getclients()).filter((entry) => entry.id !== client.id);
|
|
2393
|
+
await this.adapter.set("mcpclients", [client, ...records]);
|
|
2394
|
+
}
|
|
2395
|
+
/** Returns the connected client records — every client whose disconnect time is absent. */
|
|
2396
|
+
async listclients() {
|
|
2397
|
+
return (await this.getclients()).filter((client) => client.disconnectedat === void 0);
|
|
2398
|
+
}
|
|
2399
|
+
/** Stores the negotiated capability set of one client on its record. */
|
|
2400
|
+
async setclientcapabilities(id, capabilities) {
|
|
2401
|
+
await this.adapter.set("mcpclients", (await this.getclients()).map((client) => client.id === id ? { ...client, capabilities } : client));
|
|
2402
|
+
}
|
|
2403
|
+
/** Drops every stored client record when the server stops. */
|
|
2404
|
+
async clearclients() {
|
|
2405
|
+
await this.adapter.set("mcpclients", []);
|
|
2406
|
+
}
|
|
2407
|
+
/** Records one stdio bridge launch event with its process id; a restart marker distinguishes the relaunch of a dead client process. */
|
|
2408
|
+
async addbridgelaunch(launch) {
|
|
2409
|
+
await this.adapter.set("mcbridgelaunches", [launch, ...await this.adapter.get("mcbridgelaunches") ?? []]);
|
|
2410
|
+
}
|
|
2411
|
+
/** Returns every stdio bridge launch event, newest first. */
|
|
2412
|
+
async listbridgelaunches() {
|
|
2413
|
+
return await this.adapter.get("mcbridgelaunches") ?? [];
|
|
2414
|
+
}
|
|
2415
|
+
/** Returns the user configured mcp server config; an absent record keeps the documented localhost default. */
|
|
2416
|
+
async getmcpconfig() {
|
|
2417
|
+
return this.adapter.get("mcpconfig");
|
|
2418
|
+
}
|
|
2419
|
+
/** Stores the user configured mcp server config: bind address, port, transports, frame size, queue depth and enablement all stay user choices. */
|
|
2420
|
+
async setmcpconfig(config) {
|
|
2421
|
+
return this.adapter.set("mcpconfig", config);
|
|
2422
|
+
}
|
|
2423
|
+
/** Returns the persisted mcp server runtime state. */
|
|
2424
|
+
async getmcpstate() {
|
|
2425
|
+
return this.adapter.get("mcpstate");
|
|
2426
|
+
}
|
|
2427
|
+
/** Stores the mcp server runtime state with the stdio bridge status. */
|
|
2428
|
+
async setmcpstate(state) {
|
|
2429
|
+
return this.adapter.set("mcpstate", state);
|
|
2430
|
+
}
|
|
2431
|
+
/** Records one mcp tool call — the client, the tool, the origin and the outcome without any payload — under the user configured call retention with no code ceiling. */
|
|
2432
|
+
async addtoolcall(record2) {
|
|
2433
|
+
const records = await this.listtoolcalls();
|
|
2434
|
+
const retention = (await this.getmcpconfig())?.callretention;
|
|
2435
|
+
await this.adapter.set("mcptoolcalls", retention === void 0 ? [record2, ...records] : [record2, ...records].slice(0, retention));
|
|
2436
|
+
}
|
|
2437
|
+
/** Returns every stored mcp tool call record, newest first. */
|
|
2438
|
+
async listtoolcalls() {
|
|
2439
|
+
return await this.adapter.get("mcptoolcalls") ?? [];
|
|
2440
|
+
}
|
|
2285
2441
|
};
|
|
2286
2442
|
function mediakindof(record2) {
|
|
2287
2443
|
if ("pages" in record2) return "pdf";
|
|
@@ -2325,6 +2481,93 @@ function randomid() {
|
|
|
2325
2481
|
return crypto.randomUUID();
|
|
2326
2482
|
}
|
|
2327
2483
|
|
|
2484
|
+
// toolcatalog.ts
|
|
2485
|
+
var toolcatalogversion = 1;
|
|
2486
|
+
var toolnamespaces = ["browser", "workflow", "memory", "system"];
|
|
2487
|
+
function toolschemaof(properties) {
|
|
2488
|
+
return { type: "object", properties, required: Object.entries(properties).filter(([, property]) => property.required === true).map(([name]) => name) };
|
|
2489
|
+
}
|
|
2490
|
+
function readtool(name, kind, description, inputs = {}) {
|
|
2491
|
+
return { name, version: toolcatalogversion, description, inputschema: toolschemaof({ target: { type: "string", description: "Reviewed css selector the tool addresses." }, value: { type: "string", description: "Reviewed literal value the tool carries." }, options: { type: "object", description: "Reviewed json options of the wrapped action kind with the empty default.", default: {} }, ...inputs }), kind, risk: "read" };
|
|
2492
|
+
}
|
|
2493
|
+
function gatedtool(name, kind, risk, description, review) {
|
|
2494
|
+
return { name, version: toolcatalogversion, description, inputschema: toolschemaof({ stepid: { type: "string", description: "Id of the approved plan step this tool executes.", required: true } }), kind, risk, consentmeta: { review } };
|
|
2495
|
+
}
|
|
2496
|
+
function browserdomain() {
|
|
2497
|
+
return {
|
|
2498
|
+
namespace: "browser",
|
|
2499
|
+
version: toolcatalogversion,
|
|
2500
|
+
tools: [
|
|
2501
|
+
readtool("browser.snapshot", "observe", "Captures the semantic snapshot of the active tab: url, title, text preview, forms and interactive elements. Read only with no side effects; runs under the dryrun risk class once the session is approved."),
|
|
2502
|
+
readtool("browser.extract", "extract", "Extracts the reviewed structured data of the page. Read only with no side effects."),
|
|
2503
|
+
readtool("browser.readtext", "readtext", "Reads the text of the addressed element. Read only with no side effects.", { target: { type: "string", description: "Reviewed css selector of the element to read.", required: true } }),
|
|
2504
|
+
readtool("browser.readtable", "readtable", "Reads the rows of the addressed data table. Read only with no side effects.", { target: { type: "string", description: "Reviewed css selector of the table to read.", required: true } }),
|
|
2505
|
+
readtool("browser.readlinks", "readlinks", "Reads the link inventory of the page. Read only with no side effects."),
|
|
2506
|
+
readtool("browser.a11ytree", "a11ytree", "Reads the accessibility tree of the page. Read only with no side effects."),
|
|
2507
|
+
readtool("browser.tablist", "tablist", "Lists the open tabs. Read only with no side effects."),
|
|
2508
|
+
readtool("browser.windowlist", "windowlist", "Lists the open windows. Read only with no side effects."),
|
|
2509
|
+
gatedtool("browser.click", "click", "sensitive", "Clicks the addressed element. Sensitive: it changes page state, so it executes exactly one approved plan step.", "The click runs only as the approved plan step it names; a paired client can never widen the reviewed target or options."),
|
|
2510
|
+
gatedtool("browser.type", "type", "sensitive", "Types the reviewed text into the addressed element. Sensitive: it changes page state, so it executes exactly one approved plan step.", "The typing runs only as the approved plan step it names; the reviewed target, text and options stay fixed."),
|
|
2511
|
+
gatedtool("browser.presskey", "presskey", "sensitive", "Presses the reviewed key. Sensitive: it changes page state, so it executes exactly one approved plan step.", "The key press runs only as the approved plan step it names."),
|
|
2512
|
+
gatedtool("browser.navigate", "navigate", "sensitive", "Navigates the active tab to the reviewed url. Sensitive: it changes browser state, so it executes exactly one approved plan step.", "The navigation runs only as the approved plan step it names and stays inside the session origin grants."),
|
|
2513
|
+
gatedtool("browser.back", "back", "sensitive", "Navigates back in the history of the active tab. Sensitive: it changes browser state, so it executes exactly one approved plan step.", "The history navigation runs only as the approved plan step it names."),
|
|
2514
|
+
gatedtool("browser.forward", "forward", "sensitive", "Navigates forward in the history of the active tab. Sensitive: it changes browser state, so it executes exactly one approved plan step.", "The history navigation runs only as the approved plan step it names."),
|
|
2515
|
+
gatedtool("browser.reload", "reload", "sensitive", "Reloads the active tab. Sensitive: it changes browser state, so it executes exactly one approved plan step.", "The reload runs only as the approved plan step it names."),
|
|
2516
|
+
gatedtool("browser.tabcreate", "tabcreate", "sensitive", "Opens a new tab. Sensitive: it changes browser state, so it executes exactly one approved plan step.", "The tab creation runs only as the approved plan step it names."),
|
|
2517
|
+
gatedtool("browser.tabactivate", "tabactivate", "sensitive", "Activates the reviewed tab. Sensitive: it moves focus, so it executes exactly one approved plan step.", "The tab activation runs only as the approved plan step it names."),
|
|
2518
|
+
gatedtool("browser.tabclose", "tabclose", "sensitive", "Closes the reviewed tab. Sensitive: it destroys browser state, so it executes exactly one approved plan step.", "The tab close runs only as the approved plan step it names."),
|
|
2519
|
+
gatedtool("browser.windowcreate", "windowcreate", "sensitive", "Opens a new window. Sensitive: it changes browser state, so it executes exactly one approved plan step.", "The window creation runs only as the approved plan step it names."),
|
|
2520
|
+
gatedtool("browser.windowclose", "windowclose", "sensitive", "Closes the reviewed window. Sensitive: it destroys browser state, so it executes exactly one approved plan step.", "The window close runs only as the approved plan step it names."),
|
|
2521
|
+
gatedtool("browser.windowresize", "windowresize", "sensitive", "Resizes the reviewed window. Sensitive: it changes browser state, so it executes exactly one approved plan step.", "The window resize runs only as the approved plan step it names.")
|
|
2522
|
+
]
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
function workflowdomain() {
|
|
2526
|
+
return {
|
|
2527
|
+
namespace: "workflow",
|
|
2528
|
+
version: toolcatalogversion,
|
|
2529
|
+
tools: [
|
|
2530
|
+
readtool("workflow.list", "composeworkflow", "Lists the composed workflows with their names, versions, origins and step counts. Read only with no side effects."),
|
|
2531
|
+
readtool("workflow.dryrun", "dryrun", "Runs a composed workflow as a dry run: read steps project their would be outcome and every step with side effects is refused. Read only with no side effects."),
|
|
2532
|
+
gatedtool("workflow.run", "runworkflow", "sensitive", "Runs a composed workflow for real. Sensitive: it executes every step of the workflow, so it executes exactly one approved runworkflow plan step with its explicit run review.", "The workflow run needs the explicit run review: the approved runworkflow plan step with its expanded step list shown before the first step executes."),
|
|
2533
|
+
gatedtool("workflow.triggers", "eventrule", "sensitive", "Lists the armed trigger rules with their schedules, cooldowns and fire counters so a client can inspect what launches runs automatically. Sensitive by its trigger family: automatic launchers stay behind the arm review class.", "The trigger listing runs behind the approved plan review because trigger rules launch runs automatically.")
|
|
2534
|
+
]
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
function memorydomain() {
|
|
2538
|
+
return {
|
|
2539
|
+
namespace: "memory",
|
|
2540
|
+
version: toolcatalogversion,
|
|
2541
|
+
tools: [
|
|
2542
|
+
readtool("memory.list", "listruns", "Lists the stored workflow run records with their states and step cursors from local memory. Read only with no page access.", { target: { type: "string", description: "Unused by the memory read; kept for schema uniformity." }, value: { type: "string", description: "Unused by the memory read; kept for schema uniformity." }, state: { type: "string", description: "Optional reviewed run state filter of the listing.", default: "" } }),
|
|
2543
|
+
readtool("memory.variables", "extractvars", "Reads the stored variable scopes of a run from local memory. Read only with no page access."),
|
|
2544
|
+
readtool("memory.audit", "trailaudit", "Reads the audit summary of the session trail from local memory. Read only with no page access.")
|
|
2545
|
+
]
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
function systemdomain() {
|
|
2549
|
+
return {
|
|
2550
|
+
namespace: "system",
|
|
2551
|
+
version: toolcatalogversion,
|
|
2552
|
+
tools: [
|
|
2553
|
+
readtool("system.status", "observe", "Reports the mcp server status, the session state and the connected clients. Read only with no side effects."),
|
|
2554
|
+
readtool("system.version", "readmeta", "Reports the protocol version, the catalog version and the extension version. Read only with no side effects."),
|
|
2555
|
+
readtool("system.capabilities", "observe", "Reports the optional browser capabilities the user has granted. Read only with no side effects.")
|
|
2556
|
+
]
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
function buildtoolcatalog() {
|
|
2560
|
+
return { version: toolcatalogversion, domains: [browserdomain(), workflowdomain(), memorydomain(), systemdomain()] };
|
|
2561
|
+
}
|
|
2562
|
+
function alltools(catalog) {
|
|
2563
|
+
return catalog.domains.flatMap((domain) => domain.tools);
|
|
2564
|
+
}
|
|
2565
|
+
function resolvetool(catalog, name) {
|
|
2566
|
+
if (name.includes(".")) return alltools(catalog).find((tool) => tool.name === name);
|
|
2567
|
+
const matches = alltools(catalog).filter((tool) => tool.name.split(".")[1] === name);
|
|
2568
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2328
2571
|
// socketbus.ts
|
|
2329
2572
|
var socketkinds = ["opensocket", "sendmessage", "waitmessage", "subscribesse", "longpoll"];
|
|
2330
2573
|
function channelorigin(url) {
|
|
@@ -3942,6 +4185,14 @@ async function runcontrolstep(input) {
|
|
|
3942
4185
|
|
|
3943
4186
|
// workflow.ts
|
|
3944
4187
|
var workflowkinds = ["composeworkflow", "savetemplate", "runworkflow", "dryrun", "delay", "waitelement", "compute", "extractvars"];
|
|
4188
|
+
function nestedparamof(value) {
|
|
4189
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
4190
|
+
const candidate = value;
|
|
4191
|
+
if (typeof candidate.name !== "string" || !/^[a-z][a-z0-9]*$/.test(candidate.name)) return void 0;
|
|
4192
|
+
if (!variablekinds.includes(candidate.kind)) return void 0;
|
|
4193
|
+
if (candidate.default !== void 0 && !["string", "number", "boolean"].includes(typeof candidate.default) && !Array.isArray(candidate.default)) return void 0;
|
|
4194
|
+
return { name: candidate.name, kind: candidate.kind, ...candidate.default !== void 0 ? { default: candidate.default } : {} };
|
|
4195
|
+
}
|
|
3945
4196
|
function workflowstepof(value) {
|
|
3946
4197
|
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3947
4198
|
const candidate = value;
|
|
@@ -3951,6 +4202,7 @@ function workflowstepof(value) {
|
|
|
3951
4202
|
if (candidate.target !== void 0 && (typeof candidate.target !== "string" || !candidate.target)) return void 0;
|
|
3952
4203
|
if (candidate.value !== void 0 && typeof candidate.value !== "string") return void 0;
|
|
3953
4204
|
if (candidate.options !== void 0 && typeof candidate.options !== "string") return void 0;
|
|
4205
|
+
if (candidate.breakpoint !== void 0 && typeof candidate.breakpoint !== "boolean") return void 0;
|
|
3954
4206
|
const bindings = Array.isArray(candidate.bindings) ? candidate.bindings.flatMap((binding) => bindingof(binding) !== void 0 ? [bindingof(binding)] : []) : void 0;
|
|
3955
4207
|
if (candidate.bindings !== void 0 && bindings === void 0) return void 0;
|
|
3956
4208
|
if (Array.isArray(candidate.bindings) && bindings !== void 0 && bindings.length !== candidate.bindings.length) return void 0;
|
|
@@ -3958,14 +4210,20 @@ function workflowstepof(value) {
|
|
|
3958
4210
|
if (candidate.expression !== void 0 && expression === void 0) return void 0;
|
|
3959
4211
|
const extract = candidate.extract === void 0 ? void 0 : regexruleof(candidate.extract);
|
|
3960
4212
|
if (candidate.extract !== void 0 && extract === void 0) return void 0;
|
|
3961
|
-
|
|
4213
|
+
const params = Array.isArray(candidate.params) ? candidate.params.flatMap((param) => nestedparamof(param) !== void 0 ? [nestedparamof(param)] : []) : void 0;
|
|
4214
|
+
if (candidate.params !== void 0 && params === void 0) return void 0;
|
|
4215
|
+
if (Array.isArray(candidate.params) && params !== void 0 && params.length !== candidate.params.length) return void 0;
|
|
4216
|
+
return { id: candidate.id, kind: candidate.kind, label: candidate.label, ...candidate.target !== void 0 ? { target: candidate.target } : {}, ...candidate.value !== void 0 ? { value: candidate.value } : {}, ...candidate.options !== void 0 ? { options: candidate.options } : {}, ...bindings !== void 0 && bindings.length > 0 ? { bindings } : {}, ...expression !== void 0 ? { expression } : {}, ...extract !== void 0 ? { extract } : {}, ...candidate.breakpoint === true ? { breakpoint: true } : {}, ...params !== void 0 && params.length > 0 ? { params } : {} };
|
|
3962
4217
|
}
|
|
3963
4218
|
function blockinvocationof(value) {
|
|
3964
4219
|
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3965
4220
|
const candidate = value;
|
|
3966
4221
|
if (typeof candidate.block !== "string" || !candidate.block.trim()) return void 0;
|
|
3967
4222
|
if (typeof candidate.label !== "string" || !candidate.label.trim()) return void 0;
|
|
3968
|
-
|
|
4223
|
+
const params = Array.isArray(candidate.params) ? candidate.params.flatMap((param) => nestedparamof(param) !== void 0 ? [nestedparamof(param)] : []) : void 0;
|
|
4224
|
+
if (candidate.params !== void 0 && params === void 0) return void 0;
|
|
4225
|
+
if (Array.isArray(candidate.params) && params !== void 0 && params.length !== candidate.params.length) return void 0;
|
|
4226
|
+
return { block: candidate.block, label: candidate.label, ...params !== void 0 && params.length > 0 ? { params } : {} };
|
|
3969
4227
|
}
|
|
3970
4228
|
function workflowblockof(value) {
|
|
3971
4229
|
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
@@ -4044,10 +4302,15 @@ function regexruleof(value) {
|
|
|
4044
4302
|
function expandblocks(steps, blocks) {
|
|
4045
4303
|
const byname = new Map(blocks.map((block) => [block.name, block]));
|
|
4046
4304
|
const expanded = [];
|
|
4047
|
-
const visit = (entries, path, inside) => {
|
|
4305
|
+
const visit = (entries, path, inside, params) => {
|
|
4306
|
+
let stamped = params === void 0;
|
|
4048
4307
|
for (const entry of entries) {
|
|
4049
4308
|
if ("kind" in entry && "label" in entry && !("block" in entry)) {
|
|
4050
|
-
|
|
4309
|
+
const marked = inside === void 0 ? entry : { ...entry, block: inside };
|
|
4310
|
+
if (!stamped && params !== void 0) {
|
|
4311
|
+
expanded.push({ ...marked, params });
|
|
4312
|
+
stamped = true;
|
|
4313
|
+
} else expanded.push(marked);
|
|
4051
4314
|
continue;
|
|
4052
4315
|
}
|
|
4053
4316
|
const invocation = blockinvocationof(entry);
|
|
@@ -4055,7 +4318,7 @@ function expandblocks(steps, blocks) {
|
|
|
4055
4318
|
if (path.includes(invocation.block)) throw new Error(`The block ${invocation.block} recurs inside itself and cannot expand.`);
|
|
4056
4319
|
const block = byname.get(invocation.block);
|
|
4057
4320
|
if (!block) throw new Error(`The block ${invocation.block} is not defined in the workflow.`);
|
|
4058
|
-
visit(block.steps, [...path, invocation.block], invocation.block);
|
|
4321
|
+
visit(block.steps, [...path, invocation.block], invocation.block, invocation.params ?? params);
|
|
4059
4322
|
}
|
|
4060
4323
|
};
|
|
4061
4324
|
visit(steps, [], void 0);
|
|
@@ -4423,6 +4686,17 @@ async function runworkflow(input) {
|
|
|
4423
4686
|
if (step.block !== void 0 && step.block !== activeblock) {
|
|
4424
4687
|
scopes = pushscope(scopes, step.block, scopes[scopes.length - 1].name);
|
|
4425
4688
|
activeblock = step.block;
|
|
4689
|
+
if (step.params) {
|
|
4690
|
+
try {
|
|
4691
|
+
for (const param of step.params) {
|
|
4692
|
+
if (param.default === void 0) continue;
|
|
4693
|
+
scopes = setvariable(scopes, param.name, param.kind, coercevariable(param.default, param.kind), input.now);
|
|
4694
|
+
}
|
|
4695
|
+
} catch (error) {
|
|
4696
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
4697
|
+
return { run: { ...run, state: "failed", endedat: Date.now(), failreason: `The nested parameter of block ${step.block} failed: ${reason}` }, scopes, log, outputs };
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4426
4700
|
} else if (step.block === void 0 && activeblock !== void 0) {
|
|
4427
4701
|
while (scopes.length > 1) scopes = popscope(scopes);
|
|
4428
4702
|
activeblock = void 0;
|
|
@@ -4455,6 +4729,27 @@ function dryrunworkflow(input) {
|
|
|
4455
4729
|
}
|
|
4456
4730
|
return { run: { ...run, state: "done", cursor: input.record.steps.length, endedat: input.now }, scopes, log };
|
|
4457
4731
|
}
|
|
4732
|
+
function watchdogpass(input) {
|
|
4733
|
+
const verdicts = [];
|
|
4734
|
+
for (const run of input.runs) {
|
|
4735
|
+
if (run.state !== "running") continue;
|
|
4736
|
+
const lastcompletedat = input.lastcompletedat[run.id] ?? run.startedat;
|
|
4737
|
+
const live = input.liveexecutors.includes(run.id);
|
|
4738
|
+
const silence = input.now - lastcompletedat;
|
|
4739
|
+
if (!live && input.config.zombiewindow !== void 0 && silence >= input.config.zombiewindow) {
|
|
4740
|
+
verdicts.push({ runid: run.id, verdict: "zombie", action: "reap", reason: `The run ${run.id} lost its executor ${silence} ms ago and reaps as a zombie of a browser shutdown at its last checkpoint ${run.cursor}.`, ...lastcompletedat !== run.startedat ? { lastcompletedat } : {} });
|
|
4741
|
+
continue;
|
|
4742
|
+
}
|
|
4743
|
+
if (!live) continue;
|
|
4744
|
+
if (silence >= input.config.stallthreshold) {
|
|
4745
|
+
const action = input.config.action;
|
|
4746
|
+
verdicts.push({ runid: run.id, verdict: "stalled", action, reason: `The run ${run.id} completed no step for ${silence} ms past the reviewed threshold and the watchdog recovers it with ${action} at cursor ${run.cursor}.`, ...lastcompletedat !== run.startedat ? { lastcompletedat } : {} });
|
|
4747
|
+
continue;
|
|
4748
|
+
}
|
|
4749
|
+
verdicts.push({ runid: run.id, verdict: "healthy", action: "none", reason: `The run ${run.id} completed its last step ${silence} ms ago and stays healthy.`, ...lastcompletedat !== run.startedat ? { lastcompletedat } : {} });
|
|
4750
|
+
}
|
|
4751
|
+
return verdicts;
|
|
4752
|
+
}
|
|
4458
4753
|
|
|
4459
4754
|
// trigger.ts
|
|
4460
4755
|
var triggerkinds = ["visitrule", "urlrule", "menurule", "keyrule", "buttonrule", "cronrule", "intervalrule", "urllistrule", "webhookrule", "eventrule"];
|
|
@@ -5079,7 +5374,7 @@ function consolediff(input) {
|
|
|
5079
5374
|
// policy.ts
|
|
5080
5375
|
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract", "batchdownload", "pausedownload", "resumedownload", "interceptmime", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "cleanupartifacts", "recordscreen", "captureaudio", "downloadimages", "callrest", "callgraphql", "sendmessage", "blockrequest", "mockresponse", "rewriteheaders", "setcookies", "clearcookies", "authflow", "saveapikey", "routeproxy", "postform", "postfiles", "attachcdp", "detachcdp", "cdpcmd", "overridescript", "heapshot", "profilecpu", "capturesourcemaps", "emulatedevice", "emulatenetwork", "emulatelocate", "setuseragent", "overridepermission", "restoresession", "exportsessions", "importsessions", "runworkflow", "visitrule", "urlrule", "menurule", "keyrule", "buttonrule", "cronrule", "intervalrule", "urllistrule", "webhookrule", "eventrule"]);
|
|
5081
5376
|
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction", "capturebodies", "setbreakpoint", "stepcode", "watchexpr", "loop", "repeatuntil", "whileloop", "foreach", "parallel", "trycatch"]);
|
|
5082
|
-
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures", "shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet", "capturepdf", "captureframe", "readmedia", "readassets", "probestream", "timelapse", "shotcanvas", "convertimage", "makethumbs", "fetchurl", "parsejson", "parsehtml", "opensocket", "waitmessage", "watchrequests", "readheaders", "mapapi", "subscribesse", "longpoll", "extractapi", "readcookies", "watchconsole", "watcherrors", "watchtasks", "watchcdp", "measureflow", "trackmemory", "watchshifts", "traceload", "annotatetrace", "replaytrace", "blackboxscripts", "persiststate", "capturesession", "namedsessions", "diffsessions", "searchsessions", "composeworkflow", "savetemplate", "dryrun", "delay", "waitelement", "compute", "extractvars", "condition", "branch"]);
|
|
5377
|
+
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures", "shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet", "capturepdf", "captureframe", "readmedia", "readassets", "probestream", "timelapse", "shotcanvas", "convertimage", "makethumbs", "fetchurl", "parsejson", "parsehtml", "opensocket", "waitmessage", "watchrequests", "readheaders", "mapapi", "subscribesse", "longpoll", "extractapi", "readcookies", "watchconsole", "watcherrors", "watchtasks", "watchcdp", "measureflow", "trackmemory", "watchshifts", "traceload", "annotatetrace", "replaytrace", "blackboxscripts", "persiststate", "capturesession", "namedsessions", "diffsessions", "searchsessions", "composeworkflow", "savetemplate", "dryrun", "delay", "waitelement", "compute", "extractvars", "listruns", "condition", "branch"]);
|
|
5083
5378
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
5084
5379
|
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
|
|
5085
5380
|
var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "scrapetable", "paginateextract", "shotelement", "captureframe", "shotcanvas"]);
|
|
@@ -7838,6 +8133,165 @@ function canpreview(input) {
|
|
|
7838
8133
|
if (!targetactions.has(input.step.kind) && options.targetref === void 0) return { allowed: false, reason: "Only a target-based action can be previewed." };
|
|
7839
8134
|
return validatestep(input.step, input.origin);
|
|
7840
8135
|
}
|
|
8136
|
+
function reviewedkinds() {
|
|
8137
|
+
return [...allowedactions].sort();
|
|
8138
|
+
}
|
|
8139
|
+
function editorsavegate(input) {
|
|
8140
|
+
const gate = sessiongate({ session: input.session, tabid: input.session?.tabid ?? 0, origin: input.session?.origin ?? "https://example.com", now: input.now, action: "save the workflow editor canvas" });
|
|
8141
|
+
if (!gate.allowed) return gate;
|
|
8142
|
+
if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "Editor saves need the approved plan review before a new workflow version composes." };
|
|
8143
|
+
const model = input.model;
|
|
8144
|
+
if (typeof model.name !== "string" || !model.name.trim()) return { allowed: false, reason: "The workflow name of the canvas must be a non-empty string." };
|
|
8145
|
+
if (typeof model.version !== "number" || !Number.isInteger(model.version) || model.version < 1) return { allowed: false, reason: "The workflow version of the canvas must be a positive integer." };
|
|
8146
|
+
if (!Array.isArray(model.origins) || model.origins.length === 0) return { allowed: false, reason: "The canvas needs at least one granted HTTPS origin." };
|
|
8147
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8148
|
+
for (const node of model.nodes) {
|
|
8149
|
+
if (node.step === void 0 === (node.invocation === void 0)) return { allowed: false, reason: "Every canvas node must be exactly one workflow step or one block invocation." };
|
|
8150
|
+
const id = node.id ?? (node.step !== void 0 ? node.step.id : node.invocation.block);
|
|
8151
|
+
if (!id || ids.has(id)) return { allowed: false, reason: `The canvas node id ${id || "(empty)"} must be unique.` };
|
|
8152
|
+
ids.add(id);
|
|
8153
|
+
}
|
|
8154
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
8155
|
+
for (const node of model.nodes) {
|
|
8156
|
+
if (node.step !== void 0) {
|
|
8157
|
+
reachable.add(node.step.id);
|
|
8158
|
+
continue;
|
|
8159
|
+
}
|
|
8160
|
+
const walk = (entries) => {
|
|
8161
|
+
for (const entry of entries) {
|
|
8162
|
+
if (typeof entry.id === "string" && typeof entry.kind === "string") {
|
|
8163
|
+
reachable.add(entry.id);
|
|
8164
|
+
continue;
|
|
8165
|
+
}
|
|
8166
|
+
if (typeof entry.block === "string") {
|
|
8167
|
+
const nested = model.blocks.find((candidate) => candidate.name === entry.block);
|
|
8168
|
+
if (nested) walk(nested.steps);
|
|
8169
|
+
}
|
|
8170
|
+
}
|
|
8171
|
+
};
|
|
8172
|
+
const block = model.blocks.find((candidate) => candidate.name === node.invocation.block);
|
|
8173
|
+
if (!block) return { allowed: false, reason: `The block ${node.invocation.block} of the canvas has no definition.` };
|
|
8174
|
+
walk(block.steps);
|
|
8175
|
+
}
|
|
8176
|
+
let order = 0;
|
|
8177
|
+
const positionof = /* @__PURE__ */ new Map();
|
|
8178
|
+
for (const node of model.nodes) {
|
|
8179
|
+
if (node.step !== void 0) {
|
|
8180
|
+
positionof.set(node.step.id, order);
|
|
8181
|
+
order += 1;
|
|
8182
|
+
continue;
|
|
8183
|
+
}
|
|
8184
|
+
const walk = (entries) => {
|
|
8185
|
+
for (const entry of entries) {
|
|
8186
|
+
if (typeof entry.id === "string" && typeof entry.kind === "string") {
|
|
8187
|
+
positionof.set(entry.id, order);
|
|
8188
|
+
order += 1;
|
|
8189
|
+
continue;
|
|
8190
|
+
}
|
|
8191
|
+
if (typeof entry.block === "string") {
|
|
8192
|
+
const nested = model.blocks.find((candidate) => candidate.name === entry.block);
|
|
8193
|
+
if (nested) walk(nested.steps);
|
|
8194
|
+
}
|
|
8195
|
+
}
|
|
8196
|
+
};
|
|
8197
|
+
walk(model.blocks.find((candidate) => candidate.name === node.invocation.block).steps);
|
|
8198
|
+
}
|
|
8199
|
+
for (const edge of model.edges) {
|
|
8200
|
+
if (!reachable.has(edge.from)) return { allowed: false, reason: `The canvas edge of ${edge.variable} references the unknown source step ${edge.from}.` };
|
|
8201
|
+
if (!reachable.has(edge.to)) return { allowed: false, reason: `The canvas edge of ${edge.variable} references the unknown target step ${edge.to}.` };
|
|
8202
|
+
if ((positionof.get(edge.from) ?? -1) >= (positionof.get(edge.to) ?? -1)) return { allowed: false, reason: `The canvas edge of ${edge.variable} runs backwards and would form a cycle.` };
|
|
8203
|
+
}
|
|
8204
|
+
return { allowed: true };
|
|
8205
|
+
}
|
|
8206
|
+
function runreviewgranted(record2) {
|
|
8207
|
+
if (record2.reviewstate === "pending") return { allowed: false, reason: "The workflow stays unreviewed: the import or rollback review must approve its expanded step list before any run." };
|
|
8208
|
+
return { allowed: true };
|
|
8209
|
+
}
|
|
8210
|
+
var overrideknobs = ["loopbound", "stepms", "runms", "waitms", "delaybase"];
|
|
8211
|
+
function validatesiteoverride(override) {
|
|
8212
|
+
if (typeof override.pattern !== "string" || !override.pattern.startsWith("https://") || !/[a-z0-9.-]+/i.test(override.pattern.slice(8))) return { allowed: false, reason: "The override pattern must be an https origin or a `*` subdomain glob of one." };
|
|
8213
|
+
if (!override.pattern.includes("*")) {
|
|
8214
|
+
try {
|
|
8215
|
+
if (new URL(override.pattern).origin !== override.pattern) return { allowed: false, reason: "The override pattern must be a bare https origin or a `*` subdomain glob, never a path." };
|
|
8216
|
+
} catch {
|
|
8217
|
+
return { allowed: false, reason: "The override pattern must parse as an https origin or a `*` subdomain glob of one." };
|
|
8218
|
+
}
|
|
8219
|
+
}
|
|
8220
|
+
for (const [knob, delta] of Object.entries(override.deltas)) {
|
|
8221
|
+
if (!overrideknobs.includes(knob)) return { allowed: false, reason: `The override knob ${knob} is not one of the reviewed knobs: ${overrideknobs.join(", ")}.` };
|
|
8222
|
+
if (typeof delta !== "number" || !Number.isFinite(delta) || delta <= 0) return { allowed: false, reason: `The override delta of ${knob} must be a positive user value with no code ceiling.` };
|
|
8223
|
+
}
|
|
8224
|
+
return { allowed: true };
|
|
8225
|
+
}
|
|
8226
|
+
function exportcontentreview(file) {
|
|
8227
|
+
const secretkeys = /(secret|token|apikey|api_key|password|authorization|credential)/i;
|
|
8228
|
+
const scan = (label, options) => {
|
|
8229
|
+
if (options === void 0) return void 0;
|
|
8230
|
+
let payload;
|
|
8231
|
+
try {
|
|
8232
|
+
payload = JSON.parse(options);
|
|
8233
|
+
} catch {
|
|
8234
|
+
return void 0;
|
|
8235
|
+
}
|
|
8236
|
+
const walk = (value, path) => {
|
|
8237
|
+
if (!value || typeof value !== "object") return void 0;
|
|
8238
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
8239
|
+
if (secretkeys.test(key)) return { allowed: false, reason: `The export of ${label} carries the secret field ${path}${key} and secrets never leave the browser.` };
|
|
8240
|
+
const nested = walk(entry, `${path}${key}.`);
|
|
8241
|
+
if (nested !== void 0) return nested;
|
|
8242
|
+
}
|
|
8243
|
+
return void 0;
|
|
8244
|
+
};
|
|
8245
|
+
return walk(payload, "");
|
|
8246
|
+
};
|
|
8247
|
+
for (const step of file.workflow.steps) {
|
|
8248
|
+
const refusal = scan(`the step ${step.id}`, step.options);
|
|
8249
|
+
if (refusal !== void 0) return refusal;
|
|
8250
|
+
}
|
|
8251
|
+
for (const template of file.templates) {
|
|
8252
|
+
const refusal = scan(`the template ${template.name}`, template.step.options);
|
|
8253
|
+
if (refusal !== void 0) return refusal;
|
|
8254
|
+
}
|
|
8255
|
+
return { allowed: true };
|
|
8256
|
+
}
|
|
8257
|
+
function watchdogconfigvalid(config) {
|
|
8258
|
+
if (typeof config.enabled !== "boolean") return { allowed: false, reason: "The watchdog enabled flag must be a boolean." };
|
|
8259
|
+
if (typeof config.stallthreshold !== "number" || !Number.isFinite(config.stallthreshold) || config.stallthreshold <= 0) return { allowed: false, reason: "The watchdog stall threshold must be a positive number of milliseconds with no code ceiling." };
|
|
8260
|
+
if (!["retry", "pause", "cancel"].includes(config.action)) return { allowed: false, reason: "The watchdog recovery action must be retry, pause or cancel." };
|
|
8261
|
+
if (config.zombiewindow !== void 0 && (typeof config.zombiewindow !== "number" || !Number.isFinite(config.zombiewindow) || config.zombiewindow <= 0)) return { allowed: false, reason: "The watchdog zombie window, when configured, must be a positive number of milliseconds with no code ceiling." };
|
|
8262
|
+
return { allowed: true };
|
|
8263
|
+
}
|
|
8264
|
+
function serverbindgate(config) {
|
|
8265
|
+
const bind = config.bind !== void 0 && config.bind.trim() !== "" ? config.bind.trim() : "127.0.0.1";
|
|
8266
|
+
const local = bind === "127.0.0.1" || bind === "localhost" || bind === "::1";
|
|
8267
|
+
if (!local && config.remote !== true) return { allowed: false, reason: `The bind ${bind} leaves localhost and grades sensitive: the explicit remote review must approve it first.` };
|
|
8268
|
+
return { allowed: true };
|
|
8269
|
+
}
|
|
8270
|
+
function serverenablementgate(config) {
|
|
8271
|
+
if (config.enabled !== true) return { allowed: false, reason: "The mcp server starts only after the user enables it; the protocol surface stays closed by default." };
|
|
8272
|
+
const bind = serverbindgate(config);
|
|
8273
|
+
if (!bind.allowed) return bind;
|
|
8274
|
+
if (!Array.isArray(config.transports) || config.transports.length === 0) return { allowed: false, reason: "The mcp server needs at least one allowed transport of stdio or http." };
|
|
8275
|
+
if (!config.transports.every((transport) => transport === "stdio" || transport === "http")) return { allowed: false, reason: "The allowed transports of the mcp server are stdio and http." };
|
|
8276
|
+
if (typeof config.port !== "number" || !Number.isFinite(config.port) || config.port <= 0 || config.port > 65535) return { allowed: false, reason: "The http listener port must be a valid port number." };
|
|
8277
|
+
if (config.framesize !== void 0 && (typeof config.framesize !== "number" || !Number.isFinite(config.framesize) || config.framesize <= 0)) return { allowed: false, reason: "The user configured frame size must stay a positive number with no code ceiling." };
|
|
8278
|
+
if (config.queuedepth !== void 0 && (typeof config.queuedepth !== "number" || !Number.isFinite(config.queuedepth) || config.queuedepth <= 0)) return { allowed: false, reason: "The user configured queue depth must stay a positive number with no code ceiling." };
|
|
8279
|
+
return { allowed: true };
|
|
8280
|
+
}
|
|
8281
|
+
function tooldispatchgate(input) {
|
|
8282
|
+
if (input.client.disconnectedat !== void 0) return { allowed: false, reason: "The mcp client is disconnected and its tool calls are refused." };
|
|
8283
|
+
if (!input.client.paired) return { allowed: false, reason: "The mcp client waits for the user pairing approval; unpaired clients never dispatch tools." };
|
|
8284
|
+
if (!input.session || input.session.stoppedat || input.session.pausedat) return { allowed: false, reason: "Tool dispatch needs the live browser session behind the consent gates." };
|
|
8285
|
+
if (input.session.expiresat <= input.now) return { allowed: false, reason: "The browser session has expired and tool dispatch is refused." };
|
|
8286
|
+
if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "Tool dispatch needs the approved plan review before any tool runs." };
|
|
8287
|
+
if (!origingranted(input.session, input.origin)) return { allowed: false, reason: `The tool call origin ${input.origin} stays outside the session grants and is refused.` };
|
|
8288
|
+
if (input.tool.risk === "read") return { allowed: true };
|
|
8289
|
+
if (input.stepid === void 0 || input.stepid.trim() === "") return { allowed: false, reason: `The ${input.tool.name} tool has side effects and needs the id of the approved plan step it executes.` };
|
|
8290
|
+
const step = input.plan.steps.find((candidate) => candidate.id === input.stepid);
|
|
8291
|
+
if (step === void 0) return { allowed: false, reason: `The tool call names the step ${input.stepid} which the approved plan does not carry.` };
|
|
8292
|
+
if (step.kind !== input.tool.kind) return { allowed: false, reason: `The tool call names the step ${input.stepid} whose kind ${String(step.kind)} does not match the ${input.tool.name} tool.` };
|
|
8293
|
+
return { allowed: true };
|
|
8294
|
+
}
|
|
7841
8295
|
|
|
7842
8296
|
// progress.ts
|
|
7843
8297
|
function emptyprogress(planid, now) {
|
|
@@ -8010,9 +8464,14 @@ function recordtrigger(progress, planid, stepid, entry, now) {
|
|
|
8010
8464
|
const outcome = { stepid, ok: true, summary: `${entry.detail}${counts.length > 0 ? ` with ${counts}` : ""}.`, details: { trigger: entry }, at: now };
|
|
8011
8465
|
return recordoutcome(base, planid, outcome, now);
|
|
8012
8466
|
}
|
|
8467
|
+
function recordtoolcall(progress, planid, stepid, entry, now) {
|
|
8468
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
8469
|
+
const outcome = { stepid, ok: entry.ok, summary: `The ${entry.tool} tool call of the client ${entry.clientid} ${entry.ok ? "ran behind the consent gates" : `was refused${entry.code !== void 0 ? ` with the ${entry.code} error` : ""}`}.`, details: { tool: entry }, at: now };
|
|
8470
|
+
return recordoutcome(base, planid, outcome, now);
|
|
8471
|
+
}
|
|
8013
8472
|
|
|
8014
8473
|
// version.ts
|
|
8015
|
-
var packageversion = "1.1.
|
|
8474
|
+
var packageversion = "1.1.54";
|
|
8016
8475
|
|
|
8017
8476
|
// types.ts
|
|
8018
8477
|
var protocolversion = packageversion;
|
|
@@ -8349,7 +8808,7 @@ function requestbody(input) {
|
|
|
8349
8808
|
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
|
|
8350
8809
|
}
|
|
8351
8810
|
function outcomeresponse(input) {
|
|
8352
|
-
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {}, ...input.capture ? { capture: input.capture } : {}, ...input.media ? { media: input.media } : {}, ...input.transport ? { transport: input.transport } : {}, ...input.network ? { network: input.network } : {}, ...input.control ? { control: input.control } : {}, ...input.timeline ? { timeline: input.timeline } : {}, ...input.cdp ? { cdp: input.cdp } : {}, ...input.profile ? { profile: input.profile } : {}, ...input.emulation ? { emulation: input.emulation } : {}, ...input.session ? { session: input.session } : {}, ...input.workflow ? { workflow: { runid: input.workflow.runid, state: input.workflow.state, ...input.workflow.dryrun === true ? { dryrun: true } : {}, produced: input.workflow.produced, consumed: input.workflow.consumed, ...input.workflow.timeout !== void 0 ? { timeout: input.workflow.timeout } : {}, ...input.workflow.retry !== void 0 ? { retry: input.workflow.retry } : {} } } : {}, ...input.trigger ? { trigger: { ruleid: input.trigger.ruleid, kind: input.trigger.kind, enabled: input.trigger.enabled, ...input.trigger.nextfireat !== void 0 ? { nextfireat: input.trigger.nextfireat } : {} } } : {} });
|
|
8811
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {}, ...input.capture ? { capture: input.capture } : {}, ...input.media ? { media: input.media } : {}, ...input.transport ? { transport: input.transport } : {}, ...input.network ? { network: input.network } : {}, ...input.control ? { control: input.control } : {}, ...input.timeline ? { timeline: input.timeline } : {}, ...input.cdp ? { cdp: input.cdp } : {}, ...input.profile ? { profile: input.profile } : {}, ...input.emulation ? { emulation: input.emulation } : {}, ...input.session ? { session: input.session } : {}, ...input.workflow ? { workflow: { runid: input.workflow.runid, state: input.workflow.state, ...input.workflow.dryrun === true ? { dryrun: true } : {}, produced: input.workflow.produced, consumed: input.workflow.consumed, ...input.workflow.timeout !== void 0 ? { timeout: input.workflow.timeout } : {}, ...input.workflow.retry !== void 0 ? { retry: input.workflow.retry } : {} } } : {}, ...input.trigger ? { trigger: { ruleid: input.trigger.ruleid, kind: input.trigger.kind, enabled: input.trigger.enabled, ...input.trigger.nextfireat !== void 0 ? { nextfireat: input.trigger.nextfireat } : {} } } : {}, ...input.tool ? { tool: { clientid: input.tool.clientid, tool: input.tool.tool, origin: input.tool.origin, ok: input.tool.ok, ...input.tool.code !== void 0 ? { code: input.tool.code } : {} } } : {} });
|
|
8353
8812
|
}
|
|
8354
8813
|
function mapresponse(input) {
|
|
8355
8814
|
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
|
|
@@ -8525,6 +8984,36 @@ function triggersummaryof(rule) {
|
|
|
8525
8984
|
if (rule.schema !== void 0) summary.fields = rule.schema.length;
|
|
8526
8985
|
return summary;
|
|
8527
8986
|
}
|
|
8987
|
+
var workflowfileversion = 1;
|
|
8988
|
+
function editorstate(input) {
|
|
8989
|
+
const editor = { versions: input.versions, diffs: input.diffs ?? [], history: input.history, breakpoints: input.breakpoints ?? [], overrides: input.overrides, imports: input.imports, backgroundruns: input.backgroundruns ?? {}, watchdog: { ...input.watchdog.config !== void 0 ? { config: input.watchdog.config } : {}, events: input.watchdog.events } };
|
|
8990
|
+
return { version: protocolversion, editor, ...input.model !== void 0 ? { model: input.model } : {} };
|
|
8991
|
+
}
|
|
8992
|
+
function runhistoryquery(value) {
|
|
8993
|
+
if (value === void 0 || value === null) return {};
|
|
8994
|
+
const candidate = record(value);
|
|
8995
|
+
const query = {};
|
|
8996
|
+
if (candidate.workflowid !== void 0) {
|
|
8997
|
+
if (typeof candidate.workflowid !== "string" || !candidate.workflowid.trim()) throw new Error("The run history workflow filter must be a non-empty string.");
|
|
8998
|
+
query.workflowid = candidate.workflowid;
|
|
8999
|
+
}
|
|
9000
|
+
if (candidate.outcome !== void 0) {
|
|
9001
|
+
if (typeof candidate.outcome !== "string" || !candidate.outcome.trim()) throw new Error("The run history outcome filter must be a non-empty string.");
|
|
9002
|
+
query.outcome = candidate.outcome;
|
|
9003
|
+
}
|
|
9004
|
+
if (candidate.since !== void 0) {
|
|
9005
|
+
if (typeof candidate.since !== "number" || !Number.isFinite(candidate.since)) throw new Error("The run history time floor must be a finite timestamp.");
|
|
9006
|
+
query.since = candidate.since;
|
|
9007
|
+
}
|
|
9008
|
+
if (candidate.limit !== void 0) {
|
|
9009
|
+
if (typeof candidate.limit !== "number" || !Number.isInteger(candidate.limit) || candidate.limit < 1) throw new Error("The run history entry count must be a positive integer with no code ceiling.");
|
|
9010
|
+
query.limit = candidate.limit;
|
|
9011
|
+
}
|
|
9012
|
+
return query;
|
|
9013
|
+
}
|
|
9014
|
+
function runhistoryreport(input) {
|
|
9015
|
+
return { version: protocolversion, entries: input.entries, query: input.query ?? {} };
|
|
9016
|
+
}
|
|
8528
9017
|
|
|
8529
9018
|
// capture.ts
|
|
8530
9019
|
var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
|
|
@@ -9101,6 +9590,624 @@ async function runbrowseraction(step, sessiontabid, windowid) {
|
|
|
9101
9590
|
}
|
|
9102
9591
|
}
|
|
9103
9592
|
|
|
9593
|
+
// workfloweditor.ts
|
|
9594
|
+
var palettecategories = ["actions", "controlflow", "waits", "variables", "triggers"];
|
|
9595
|
+
var palettenodes = [
|
|
9596
|
+
{ kind: "click", label: "Click an element", category: "actions", description: "Clicks the reviewed selector target." },
|
|
9597
|
+
{ kind: "type", label: "Type text", category: "actions", description: "Types the reviewed text into the target field." },
|
|
9598
|
+
{ kind: "navigate", label: "Navigate", category: "actions", description: "Navigates the tab to the reviewed url." },
|
|
9599
|
+
{ kind: "readtext", label: "Read text", category: "actions", description: "Reads the text of the target element." },
|
|
9600
|
+
{ kind: "scrapetable", label: "Scrape a table", category: "actions", description: "Extracts the reviewed table into a dataset." },
|
|
9601
|
+
{ kind: "fillform", label: "Fill a form", category: "actions", description: "Fills the reviewed form fields from a saved profile." },
|
|
9602
|
+
{ kind: "querytabs", label: "Query tabs", category: "actions", description: "Lists the tabs matching the reviewed query." },
|
|
9603
|
+
{ kind: "fetchurl", label: "Fetch a url", category: "actions", description: "Fetches the reviewed endpoint behind the call consent." },
|
|
9604
|
+
{ kind: "condition", label: "Condition", category: "controlflow", description: "Evaluates one reviewed boolean expression with no page side effect." },
|
|
9605
|
+
{ kind: "branch", label: "Branch", category: "controlflow", description: "Chooses one reviewed path by page state with a mandatory else path." },
|
|
9606
|
+
{ kind: "loop", label: "Loop a list", category: "controlflow", description: "Iterates a list variable binding the item and index per pass." },
|
|
9607
|
+
{ kind: "repeatuntil", label: "Repeat until", category: "controlflow", description: "Reruns the body until the convergence expression holds." },
|
|
9608
|
+
{ kind: "whileloop", label: "While loop", category: "controlflow", description: "Loops while the condition holds inside the reviewed bound." },
|
|
9609
|
+
{ kind: "foreach", label: "For each element", category: "controlflow", description: "Iterates the elements of the reviewed selector." },
|
|
9610
|
+
{ kind: "parallel", label: "Parallel branches", category: "controlflow", description: "Runs branches concurrently and joins them under the reviewed strategy." },
|
|
9611
|
+
{ kind: "trycatch", label: "Try catch", category: "controlflow", description: "Wraps fragile steps with a catch handler, retries and timeouts." },
|
|
9612
|
+
{ kind: "delay", label: "Delay", category: "waits", description: "Sleeps the reviewed base inside the jitter window." },
|
|
9613
|
+
{ kind: "waitelement", label: "Wait for element", category: "waits", description: "Polls the reviewed selector until appearance or timeout." },
|
|
9614
|
+
{ kind: "wait", label: "Wait", category: "waits", description: "Waits the reviewed duration." },
|
|
9615
|
+
{ kind: "waitfor", label: "Wait for target", category: "waits", description: "Waits until the reviewed target exists." },
|
|
9616
|
+
{ kind: "waittext", label: "Wait for text", category: "waits", description: "Waits until the reviewed text appears." },
|
|
9617
|
+
{ kind: "waitquiet", label: "Wait for quiet", category: "waits", description: "Waits until the page stops mutating." },
|
|
9618
|
+
{ kind: "waitload", label: "Wait for load", category: "waits", description: "Waits until the navigation settles." },
|
|
9619
|
+
{ kind: "compute", label: "Compute", category: "variables", description: "Evaluates one reviewed expression into the result variable." },
|
|
9620
|
+
{ kind: "extractvars", label: "Extract variables", category: "variables", description: "Applies the reviewed regex and stores the named captures." },
|
|
9621
|
+
{ kind: "savetemplate", label: "Save template", category: "variables", description: "Shares the reviewed step as a reusable template." },
|
|
9622
|
+
{ kind: "visitrule", label: "Visit rule", category: "triggers", description: "Fires on navigations to the reviewed origins." },
|
|
9623
|
+
{ kind: "urlrule", label: "Url rule", category: "triggers", description: "Fires when the url matches the reviewed glob pattern." },
|
|
9624
|
+
{ kind: "cronrule", label: "Cron rule", category: "triggers", description: "Fires on the reviewed five field cron schedule." },
|
|
9625
|
+
{ kind: "intervalrule", label: "Interval rule", category: "triggers", description: "Fires every reviewed period with the jitter spread." },
|
|
9626
|
+
{ kind: "webhookrule", label: "Webhook rule", category: "triggers", description: "Fires on a secret verified webhook delivery." },
|
|
9627
|
+
{ kind: "eventrule", label: "Event rule", category: "triggers", description: "Fires on the observed page events of the catalog." }
|
|
9628
|
+
];
|
|
9629
|
+
var optionschemas = {
|
|
9630
|
+
delay: [{ name: "base", kind: "number", required: true }, { name: "jitter", kind: "number" }],
|
|
9631
|
+
waitelement: [{ name: "timeout", kind: "number" }, { name: "poll", kind: "number" }],
|
|
9632
|
+
compute: [{ name: "expression", kind: "string", required: true }],
|
|
9633
|
+
extractvars: [{ name: "rule", kind: "string", required: true }],
|
|
9634
|
+
composeworkflow: [{ name: "name", kind: "string", required: true }, { name: "version", kind: "number" }],
|
|
9635
|
+
runworkflow: [{ name: "workflowid", kind: "string", required: true }, { name: "reviewed", kind: "boolean", required: true }, { name: "variables", kind: "string" }, { name: "background", kind: "boolean" }],
|
|
9636
|
+
dryrun: [{ name: "workflowid", kind: "string", required: true }],
|
|
9637
|
+
loop: [{ name: "loop", kind: "string", required: true }],
|
|
9638
|
+
repeatuntil: [{ name: "repeatuntil", kind: "string", required: true }],
|
|
9639
|
+
whileloop: [{ name: "whileloop", kind: "string", required: true }],
|
|
9640
|
+
foreach: [{ name: "foreach", kind: "string", required: true }],
|
|
9641
|
+
parallel: [{ name: "parallel", kind: "string", required: true }],
|
|
9642
|
+
trycatch: [{ name: "trycatch", kind: "string", required: true }]
|
|
9643
|
+
};
|
|
9644
|
+
function stepcategory(kind) {
|
|
9645
|
+
if (triggerkinds.includes(kind)) return "triggers";
|
|
9646
|
+
if (controlflowkinds.includes(kind)) return "controlflow";
|
|
9647
|
+
if (kind.startsWith("wait") || kind === "spawait" || kind === "delay") return "waits";
|
|
9648
|
+
if (kind === "compute" || kind === "extractvars" || kind === "savetemplate") return "variables";
|
|
9649
|
+
return "actions";
|
|
9650
|
+
}
|
|
9651
|
+
function buildsteplibrary(kinds) {
|
|
9652
|
+
return [...new Set(kinds)].sort().map((kind) => ({ kind, category: stepcategory(kind), optionschema: optionschemas[kind] ?? [] }));
|
|
9653
|
+
}
|
|
9654
|
+
var noderowheight = 96;
|
|
9655
|
+
var blockcolumnwidth = 280;
|
|
9656
|
+
var canvasoriginx = 40;
|
|
9657
|
+
function nodeidof(node) {
|
|
9658
|
+
return node.id ?? (node.step !== void 0 ? node.step.id : node.invocation !== void 0 ? node.invocation.block : "");
|
|
9659
|
+
}
|
|
9660
|
+
function layoutsizeof(nodes) {
|
|
9661
|
+
const width = Math.max(640, ...nodes.map((node) => node.x + blockcolumnwidth)) + 40;
|
|
9662
|
+
const height = Math.max(480, ...nodes.map((node) => node.y + noderowheight)) + 40;
|
|
9663
|
+
return { width, height };
|
|
9664
|
+
}
|
|
9665
|
+
function loadworkflow(record2, layout) {
|
|
9666
|
+
const blocks = record2.blocks.map((block) => ({ ...block, steps: block.steps.map((entry) => ({ ...entry })) }));
|
|
9667
|
+
const blockcolumn = (blockname) => {
|
|
9668
|
+
const index2 = blocks.findIndex((block) => block.name === blockname);
|
|
9669
|
+
return index2 < 0 ? canvasoriginx : canvasoriginx + (index2 + 1) * blockcolumnwidth;
|
|
9670
|
+
};
|
|
9671
|
+
const invocationcount = /* @__PURE__ */ new Map();
|
|
9672
|
+
const nodes = [];
|
|
9673
|
+
const edges = [];
|
|
9674
|
+
let index = 0;
|
|
9675
|
+
while (index < record2.steps.length) {
|
|
9676
|
+
const step = record2.steps[index];
|
|
9677
|
+
for (const binding of step.bindings ?? []) edges.push({ from: binding.stepid, to: step.id, variable: binding.variable, kind: binding.kind, ...binding.path !== void 0 ? { path: binding.path } : {} });
|
|
9678
|
+
if (step.block === void 0) {
|
|
9679
|
+
const { bindings, block, params: params2, ...rest } = step;
|
|
9680
|
+
void bindings;
|
|
9681
|
+
void block;
|
|
9682
|
+
void params2;
|
|
9683
|
+
nodes.push({ step: { ...rest }, x: canvasoriginx, y: 60 + nodes.length * noderowheight });
|
|
9684
|
+
index += 1;
|
|
9685
|
+
continue;
|
|
9686
|
+
}
|
|
9687
|
+
const blockname = step.block;
|
|
9688
|
+
let end = index;
|
|
9689
|
+
while (end < record2.steps.length && record2.steps[end].block === blockname) end += 1;
|
|
9690
|
+
const region = record2.steps.slice(index, end);
|
|
9691
|
+
const count = (invocationcount.get(blockname) ?? 0) + 1;
|
|
9692
|
+
invocationcount.set(blockname, count);
|
|
9693
|
+
const params = region.flatMap((entry) => entry.params ?? []);
|
|
9694
|
+
nodes.push({ id: count === 1 ? blockname : `${blockname}${count}`, invocation: { block: blockname, label: blockname, ...params.length > 0 ? { params: params.map((param) => ({ ...param })) } : {} }, x: blockcolumn(blockname), y: 60 + nodes.length * noderowheight });
|
|
9695
|
+
index = end;
|
|
9696
|
+
}
|
|
9697
|
+
const size = layouttypeof(nodes, layout);
|
|
9698
|
+
const model = { workflowid: record2.id, name: record2.name, version: record2.version, origins: [...record2.origins], nodes, edges, blocks, layout: size, minimap: emptyminimap(), dirty: false };
|
|
9699
|
+
return { ...model, minimap: renderminimap(model).minimap };
|
|
9700
|
+
}
|
|
9701
|
+
function layouttypeof(nodes, layout) {
|
|
9702
|
+
const size = layoutsizeof(nodes);
|
|
9703
|
+
if (!layout) return { width: size.width, height: size.height, viewportx: 0, viewporty: 0, zoom: 1 };
|
|
9704
|
+
return { width: Math.max(size.width, layout.width), height: Math.max(size.height, layout.height), viewportx: layout.viewportx, viewporty: layout.viewporty, zoom: layout.zoom };
|
|
9705
|
+
}
|
|
9706
|
+
function emptyminimap() {
|
|
9707
|
+
return { width: 160, height: 100, scale: 0, zoom: 1, viewport: { x: 0, y: 0, width: 0, height: 0 } };
|
|
9708
|
+
}
|
|
9709
|
+
function saveworkflow(model, input) {
|
|
9710
|
+
if (typeof model.name !== "string" || !model.name.trim()) throw new Error("The workflow name must be a non-empty string.");
|
|
9711
|
+
if (typeof model.version !== "number" || !Number.isInteger(model.version) || model.version < 1) throw new Error("The workflow version must be a positive integer.");
|
|
9712
|
+
if (!Array.isArray(model.origins) || model.origins.length === 0) throw new Error("A workflow needs at least one granted HTTPS origin.");
|
|
9713
|
+
const ids = /* @__PURE__ */ new Set();
|
|
9714
|
+
for (const node of model.nodes) {
|
|
9715
|
+
if (node.step === void 0 === (node.invocation === void 0)) throw new Error("Every canvas node must be exactly one workflow step or one block invocation.");
|
|
9716
|
+
const id = nodeidof(node);
|
|
9717
|
+
if (!id || ids.has(id)) throw new Error(`The canvas node id ${id || "(empty)"} must be unique.`);
|
|
9718
|
+
ids.add(id);
|
|
9719
|
+
}
|
|
9720
|
+
const positionof = /* @__PURE__ */ new Map();
|
|
9721
|
+
let position = 0;
|
|
9722
|
+
for (const node of model.nodes) {
|
|
9723
|
+
if (node.step !== void 0) {
|
|
9724
|
+
positionof.set(node.step.id, position);
|
|
9725
|
+
position += 1;
|
|
9726
|
+
continue;
|
|
9727
|
+
}
|
|
9728
|
+
const block = model.blocks.find((entry) => entry.name === node.invocation?.block);
|
|
9729
|
+
if (!block) throw new Error(`The block ${node.invocation?.block ?? ""} of the canvas has no definition.`);
|
|
9730
|
+
const walk = (entries2) => {
|
|
9731
|
+
for (const entry of entries2) {
|
|
9732
|
+
if ("kind" in entry && "label" in entry && !("block" in entry)) {
|
|
9733
|
+
positionof.set(entry.id, position);
|
|
9734
|
+
position += 1;
|
|
9735
|
+
continue;
|
|
9736
|
+
}
|
|
9737
|
+
const nested = model.blocks.find((candidate) => candidate.name === entry.block);
|
|
9738
|
+
if (!nested) throw new Error(`The block ${entry.block} of the canvas has no definition.`);
|
|
9739
|
+
walk(nested.steps);
|
|
9740
|
+
}
|
|
9741
|
+
};
|
|
9742
|
+
walk(block.steps);
|
|
9743
|
+
}
|
|
9744
|
+
for (const edge of model.edges) {
|
|
9745
|
+
if (!positionof.has(edge.from)) throw new Error(`The edge of ${edge.variable} references the unknown source step ${edge.from}.`);
|
|
9746
|
+
if (!positionof.has(edge.to)) throw new Error(`The edge of ${edge.variable} references the unknown target step ${edge.to}.`);
|
|
9747
|
+
if (positionof.get(edge.from) >= positionof.get(edge.to)) throw new Error(`The edge of ${edge.variable} runs backwards from ${edge.from} into ${edge.to} and would form a cycle.`);
|
|
9748
|
+
}
|
|
9749
|
+
const bindingsof = (stepid) => model.edges.filter((edge) => edge.to === stepid).map((edge) => ({ variable: edge.variable, kind: edge.kind, stepid: edge.from, ...edge.path !== void 0 ? { path: edge.path } : {} }));
|
|
9750
|
+
const entries = [];
|
|
9751
|
+
const attached = /* @__PURE__ */ new Map();
|
|
9752
|
+
for (const node of model.nodes) {
|
|
9753
|
+
if (node.invocation !== void 0) {
|
|
9754
|
+
entries.push({ ...node.invocation });
|
|
9755
|
+
continue;
|
|
9756
|
+
}
|
|
9757
|
+
const step = node.step;
|
|
9758
|
+
const bindings = bindingsof(step.id);
|
|
9759
|
+
const { block, params, ...rest } = { ...step, ...bindings.length > 0 ? { bindings } : {} };
|
|
9760
|
+
void params;
|
|
9761
|
+
const carried = rest;
|
|
9762
|
+
if (block !== void 0) {
|
|
9763
|
+
if (!model.blocks.some((candidate) => candidate.name === block)) throw new Error(`The step ${step.id} attaches to the unknown block ${block}.`);
|
|
9764
|
+
const list = attached.get(block) ?? [];
|
|
9765
|
+
list.push(carried);
|
|
9766
|
+
attached.set(block, list);
|
|
9767
|
+
continue;
|
|
9768
|
+
}
|
|
9769
|
+
entries.push(carried);
|
|
9770
|
+
}
|
|
9771
|
+
const blocks = model.blocks.map((block) => {
|
|
9772
|
+
const snapped = attached.get(block.name) ?? [];
|
|
9773
|
+
const snappedids = new Set(snapped.map((step) => step.id));
|
|
9774
|
+
const carried = [];
|
|
9775
|
+
for (const entry of block.steps) {
|
|
9776
|
+
if ("kind" in entry && "label" in entry && !("block" in entry) && snappedids.has(entry.id)) continue;
|
|
9777
|
+
carried.push(entry);
|
|
9778
|
+
}
|
|
9779
|
+
const steps = [...carried, ...snapped];
|
|
9780
|
+
const withbindings = [];
|
|
9781
|
+
for (const entry of steps) {
|
|
9782
|
+
if (!("kind" in entry && "label" in entry && !("block" in entry))) {
|
|
9783
|
+
withbindings.push(entry);
|
|
9784
|
+
continue;
|
|
9785
|
+
}
|
|
9786
|
+
const bindings = bindingsof(entry.id);
|
|
9787
|
+
const { block: inner, params, ...rest } = { ...entry, ...bindings.length > 0 ? { bindings } : {} };
|
|
9788
|
+
void inner;
|
|
9789
|
+
void params;
|
|
9790
|
+
withbindings.push(rest);
|
|
9791
|
+
}
|
|
9792
|
+
return { ...block, steps: withbindings };
|
|
9793
|
+
});
|
|
9794
|
+
const composed = composeworkflow({ id: model.workflowid, name: model.name, version: model.version, origins: [...model.origins], steps: entries, blocks: blocks.map((block) => ({ ...block })), now: input.now, ...input.kindallowed !== void 0 ? { kindallowed: input.kindallowed } : {}, ...input.riskof !== void 0 ? { riskof: input.riskof } : {} });
|
|
9795
|
+
const checked = validateworkflow(composed, input.kindallowed !== void 0 ? { kindallowed: input.kindallowed } : {});
|
|
9796
|
+
if (!checked.allowed) throw new Error(checked.reason ?? "The canvas model failed the workflow grammar.");
|
|
9797
|
+
return composed;
|
|
9798
|
+
}
|
|
9799
|
+
function renderminimap(model, width = 160, height = 100) {
|
|
9800
|
+
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) throw new Error("The mini map size must be positive.");
|
|
9801
|
+
const canvaswidth = Math.max(1, model.layout.width);
|
|
9802
|
+
const canvasheight = Math.max(1, model.layout.height);
|
|
9803
|
+
const scale = Math.min(width / canvaswidth, height / canvasheight);
|
|
9804
|
+
const zoom = model.layout.zoom > 0 ? model.layout.zoom : 1;
|
|
9805
|
+
const visiblewidth = canvaswidth / zoom;
|
|
9806
|
+
const visibleheight = canvasheight / zoom;
|
|
9807
|
+
const viewport = {
|
|
9808
|
+
x: Math.max(0, Math.min(model.layout.viewportx, canvaswidth)) * scale,
|
|
9809
|
+
y: Math.max(0, Math.min(model.layout.viewporty, canvasheight)) * scale,
|
|
9810
|
+
width: visiblewidth * scale,
|
|
9811
|
+
height: visibleheight * scale
|
|
9812
|
+
};
|
|
9813
|
+
const nodes = model.nodes.map((node) => ({ id: nodeidof(node), x: node.x * scale, y: node.y * scale }));
|
|
9814
|
+
return { minimap: { width, height, scale, zoom, viewport }, nodes };
|
|
9815
|
+
}
|
|
9816
|
+
function runtobreakpoint(input) {
|
|
9817
|
+
const cursor = input.cursor !== void 0 && Number.isInteger(input.cursor) && input.cursor >= 0 ? input.cursor : 0;
|
|
9818
|
+
const marked = new Set(input.breakpoints);
|
|
9819
|
+
for (let index = cursor; index < input.record.steps.length; index += 1) {
|
|
9820
|
+
const step = input.record.steps[index];
|
|
9821
|
+
if (step.breakpoint === true || marked.has(step.id)) {
|
|
9822
|
+
return { until: index, pausat: step.id, remaining: input.record.steps.length - index };
|
|
9823
|
+
}
|
|
9824
|
+
}
|
|
9825
|
+
return { until: input.record.steps.length, pausat: void 0, remaining: 0 };
|
|
9826
|
+
}
|
|
9827
|
+
function diffversions(from, to, now) {
|
|
9828
|
+
const fromsteps = new Map(from.steps.map((step) => [step.id, step]));
|
|
9829
|
+
const tosteps = new Map(to.steps.map((step) => [step.id, step]));
|
|
9830
|
+
const added = [];
|
|
9831
|
+
const removed = [];
|
|
9832
|
+
const changed = [];
|
|
9833
|
+
for (const step of to.steps) {
|
|
9834
|
+
const prior = fromsteps.get(step.id);
|
|
9835
|
+
if (!prior) {
|
|
9836
|
+
added.push({ stepid: step.id, kind: step.kind, label: step.label });
|
|
9837
|
+
continue;
|
|
9838
|
+
}
|
|
9839
|
+
const changes = [];
|
|
9840
|
+
if (prior.label !== step.label) changes.push("label");
|
|
9841
|
+
if (prior.kind !== step.kind) changes.push("kind");
|
|
9842
|
+
if (prior.target !== step.target) changes.push("target");
|
|
9843
|
+
if (prior.value !== step.value) changes.push("value");
|
|
9844
|
+
if (prior.options !== step.options) changes.push("options");
|
|
9845
|
+
if (JSON.stringify(prior.expression) !== JSON.stringify(step.expression)) changes.push("expression");
|
|
9846
|
+
if (JSON.stringify(prior.extract) !== JSON.stringify(step.extract)) changes.push("extract");
|
|
9847
|
+
if (JSON.stringify(prior.bindings) !== JSON.stringify(step.bindings)) changes.push("bindings");
|
|
9848
|
+
if (changes.length > 0) changed.push({ stepid: step.id, kind: step.kind, label: step.label, changes });
|
|
9849
|
+
}
|
|
9850
|
+
for (const step of from.steps) {
|
|
9851
|
+
if (!tosteps.has(step.id)) removed.push({ stepid: step.id, kind: step.kind, label: step.label });
|
|
9852
|
+
}
|
|
9853
|
+
return { workflowid: to.id, from: from.version, to: to.version, added, removed, changed, at: now };
|
|
9854
|
+
}
|
|
9855
|
+
function exportworkflow(record2, format, note, now) {
|
|
9856
|
+
const file = { format: workflowfileversion, exportedat: now ?? Date.now(), workflow: record2, ...note !== void 0 && note.trim() !== "" ? { note } : {}, templates: [] };
|
|
9857
|
+
return { format, contents: serializefile(file, format), file };
|
|
9858
|
+
}
|
|
9859
|
+
function shareworkflow(record2, templates, format, note, now) {
|
|
9860
|
+
const file = { format: workflowfileversion, exportedat: now ?? Date.now(), workflow: record2, ...note !== void 0 && note.trim() !== "" ? { note } : {}, templates: templates.map((template) => ({ ...template })) };
|
|
9861
|
+
return { format, contents: serializefile(file, format), file };
|
|
9862
|
+
}
|
|
9863
|
+
function importworkflow(input) {
|
|
9864
|
+
const format = input.format ?? (input.contents.trimStart().startsWith("{") ? "json" : "yaml");
|
|
9865
|
+
const parsed = parsefile(input.contents, format);
|
|
9866
|
+
if (parsed.format !== workflowfileversion) throw new Error(`The workflow file format ${String(parsed.format)} is not the reviewed format ${workflowfileversion}.`);
|
|
9867
|
+
const candidate = parsed.workflow;
|
|
9868
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) throw new Error("The workflow file carries no workflow record.");
|
|
9869
|
+
const fields = candidate;
|
|
9870
|
+
const stepsvalue = fields.steps;
|
|
9871
|
+
if (!Array.isArray(stepsvalue) || stepsvalue.length === 0) throw new Error("An imported workflow needs at least one step.");
|
|
9872
|
+
const steps = [];
|
|
9873
|
+
for (const entry of stepsvalue) {
|
|
9874
|
+
const step = workflowstepof(entry);
|
|
9875
|
+
if (step) {
|
|
9876
|
+
steps.push(step);
|
|
9877
|
+
continue;
|
|
9878
|
+
}
|
|
9879
|
+
throw new Error("Every imported workflow entry must be a reviewed step.");
|
|
9880
|
+
}
|
|
9881
|
+
const composed = composeworkflow({
|
|
9882
|
+
id: typeof fields.id === "string" && fields.id.trim() !== "" ? fields.id : crypto.randomUUID(),
|
|
9883
|
+
name: typeof fields.name === "string" ? fields.name : "",
|
|
9884
|
+
version: typeof fields.version === "number" ? fields.version : 1,
|
|
9885
|
+
origins: Array.isArray(fields.origins) ? fields.origins.filter((origin) => typeof origin === "string") : [],
|
|
9886
|
+
steps,
|
|
9887
|
+
now: input.now ?? Date.now(),
|
|
9888
|
+
...input.kindallowed !== void 0 ? { kindallowed: input.kindallowed } : {},
|
|
9889
|
+
...input.riskof !== void 0 ? { riskof: input.riskof } : {}
|
|
9890
|
+
});
|
|
9891
|
+
const templatesvalue = parsed.templates;
|
|
9892
|
+
if (templatesvalue !== void 0 && !Array.isArray(templatesvalue)) throw new Error("The packed templates of the workflow file must be a list.");
|
|
9893
|
+
const templates = [];
|
|
9894
|
+
for (const entry of templatesvalue ?? []) {
|
|
9895
|
+
const template = steptemplateof(entry);
|
|
9896
|
+
if (!template) throw new Error("A packed template of the workflow file does not carry one reviewed step.");
|
|
9897
|
+
templates.push(template);
|
|
9898
|
+
}
|
|
9899
|
+
const record2 = { ...composed, reviewstate: "pending" };
|
|
9900
|
+
return { record: record2, templates, file: { ...parsed, workflow: record2 } };
|
|
9901
|
+
}
|
|
9902
|
+
function originmatches(pattern, origin) {
|
|
9903
|
+
if (pattern === origin) return true;
|
|
9904
|
+
const glob = pattern.replace(/\./g, "\\.").replace(/\*/g, "[^.]+");
|
|
9905
|
+
if (!glob.startsWith("https://")) return false;
|
|
9906
|
+
return new RegExp(`^${glob}$`).test(origin);
|
|
9907
|
+
}
|
|
9908
|
+
function applyoverride(record2, override) {
|
|
9909
|
+
const matching = record2.origins.filter((origin) => originmatches(override.pattern, origin));
|
|
9910
|
+
if (matching.length === 0) throw new Error(`The override pattern ${override.pattern} matches none of the workflow origins ${record2.origins.join(", ")}.`);
|
|
9911
|
+
const knobs = /* @__PURE__ */ new Set(["loopbound", "stepms", "runms", "waitms", "delaybase"]);
|
|
9912
|
+
for (const knob of Object.keys(override.deltas)) {
|
|
9913
|
+
if (!knobs.has(knob)) throw new Error(`The override knob ${knob} is not one of the reviewed knobs: ${[...knobs].join(", ")}.`);
|
|
9914
|
+
if (typeof override.deltas[knob] !== "number" || !Number.isFinite(override.deltas[knob]) || override.deltas[knob] <= 0) throw new Error(`The override delta of ${knob} must be a positive number with no code ceiling.`);
|
|
9915
|
+
}
|
|
9916
|
+
const apply = (step) => {
|
|
9917
|
+
if (Object.keys(override.deltas).length === 0) return step;
|
|
9918
|
+
let payload = {};
|
|
9919
|
+
try {
|
|
9920
|
+
payload = step.options !== void 0 ? JSON.parse(step.options) : {};
|
|
9921
|
+
} catch {
|
|
9922
|
+
payload = {};
|
|
9923
|
+
}
|
|
9924
|
+
const bodyof = (key) => payload[key] !== void 0 && typeof payload[key] === "object" && !Array.isArray(payload[key]) ? payload[key] : {};
|
|
9925
|
+
if (override.deltas.loopbound !== void 0 && ["loop", "repeatuntil", "whileloop"].includes(step.kind)) {
|
|
9926
|
+
const body = bodyof(step.kind);
|
|
9927
|
+
body.bound = override.deltas.loopbound;
|
|
9928
|
+
payload[step.kind] = body;
|
|
9929
|
+
}
|
|
9930
|
+
if ((override.deltas.stepms !== void 0 || override.deltas.runms !== void 0) && step.kind === "trycatch") {
|
|
9931
|
+
const body = bodyof("trycatch");
|
|
9932
|
+
const timeout = body.timeout !== void 0 && typeof body.timeout === "object" && !Array.isArray(body.timeout) ? body.timeout : {};
|
|
9933
|
+
if (override.deltas.stepms !== void 0) timeout.stepms = override.deltas.stepms;
|
|
9934
|
+
if (override.deltas.runms !== void 0) timeout.runms = override.deltas.runms;
|
|
9935
|
+
body.timeout = timeout;
|
|
9936
|
+
payload.trycatch = body;
|
|
9937
|
+
}
|
|
9938
|
+
if (override.deltas.waitms !== void 0 && step.kind === "waitelement") {
|
|
9939
|
+
payload.timeout = override.deltas.waitms;
|
|
9940
|
+
}
|
|
9941
|
+
if (override.deltas.delaybase !== void 0 && step.kind === "delay") {
|
|
9942
|
+
payload.base = override.deltas.delaybase;
|
|
9943
|
+
}
|
|
9944
|
+
const changed = Object.keys(payload).length > 0;
|
|
9945
|
+
return changed ? { ...step, options: JSON.stringify(payload) } : step;
|
|
9946
|
+
};
|
|
9947
|
+
return { ...record2, steps: record2.steps.map(apply) };
|
|
9948
|
+
}
|
|
9949
|
+
function serializefile(file, format) {
|
|
9950
|
+
if (format === "json") return JSON.stringify(file, null, 2);
|
|
9951
|
+
return yamlvalue(file, 0).join("\n") + "\n";
|
|
9952
|
+
}
|
|
9953
|
+
function parsefile(contents, format) {
|
|
9954
|
+
if (format === "json") {
|
|
9955
|
+
const parsed = JSON.parse(contents);
|
|
9956
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("The workflow file is not a json object.");
|
|
9957
|
+
return parsed;
|
|
9958
|
+
}
|
|
9959
|
+
const lines = contents.split(/\r?\n/).map((line) => line.replace(/\t/g, " ")).filter((line) => line.trim() !== "" && !line.trim().startsWith("#"));
|
|
9960
|
+
if (lines.length === 0) throw new Error("The yaml workflow file is empty.");
|
|
9961
|
+
const { value, next } = yamlblock(lines, 0, indentof(lines[0]));
|
|
9962
|
+
if (next < lines.length) throw new Error("The yaml workflow file carries content outside the documented subset.");
|
|
9963
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("The yaml workflow file is not a mapping.");
|
|
9964
|
+
return value;
|
|
9965
|
+
}
|
|
9966
|
+
function indentof(line) {
|
|
9967
|
+
const match = /^ */.exec(line);
|
|
9968
|
+
return match ? match[0].length : 0;
|
|
9969
|
+
}
|
|
9970
|
+
function yamlscalar(value) {
|
|
9971
|
+
if (value === null || value === void 0) return "null";
|
|
9972
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
9973
|
+
return JSON.stringify(String(value));
|
|
9974
|
+
}
|
|
9975
|
+
function yamlvalue(value, indent) {
|
|
9976
|
+
const pad = " ".repeat(indent);
|
|
9977
|
+
if (value === null || value === void 0 || typeof value !== "object") return [`${pad}${yamlscalar(value)}`];
|
|
9978
|
+
if (Array.isArray(value)) {
|
|
9979
|
+
if (value.length === 0) return [`${pad}[]`];
|
|
9980
|
+
const lines2 = [];
|
|
9981
|
+
for (const item of value) {
|
|
9982
|
+
if (item !== null && typeof item === "object") {
|
|
9983
|
+
lines2.push(`${pad}-`);
|
|
9984
|
+
lines2.push(...yamlvalue(item, indent + 2));
|
|
9985
|
+
} else {
|
|
9986
|
+
lines2.push(`${pad}- ${yamlscalar(item)}`);
|
|
9987
|
+
}
|
|
9988
|
+
}
|
|
9989
|
+
return lines2;
|
|
9990
|
+
}
|
|
9991
|
+
const entries = Object.entries(value);
|
|
9992
|
+
if (entries.length === 0) return [`${pad}{}`];
|
|
9993
|
+
const lines = [];
|
|
9994
|
+
for (const [key, entry] of entries) {
|
|
9995
|
+
if (entry !== null && typeof entry === "object") {
|
|
9996
|
+
if (Array.isArray(entry) && entry.length === 0) {
|
|
9997
|
+
lines.push(`${pad}${key}: []`);
|
|
9998
|
+
continue;
|
|
9999
|
+
}
|
|
10000
|
+
if (!Array.isArray(entry) && Object.keys(entry).length === 0) {
|
|
10001
|
+
lines.push(`${pad}${key}: {}`);
|
|
10002
|
+
continue;
|
|
10003
|
+
}
|
|
10004
|
+
lines.push(`${pad}${key}:`);
|
|
10005
|
+
lines.push(...yamlvalue(entry, indent + 2));
|
|
10006
|
+
} else {
|
|
10007
|
+
lines.push(`${pad}${key}: ${yamlscalar(entry)}`);
|
|
10008
|
+
}
|
|
10009
|
+
}
|
|
10010
|
+
return lines;
|
|
10011
|
+
}
|
|
10012
|
+
function yamlblock(lines, start, indent) {
|
|
10013
|
+
const first = lines[start];
|
|
10014
|
+
if (/^\s*-\s/.test(first) || /^\s*-$/.test(first)) {
|
|
10015
|
+
const items = [];
|
|
10016
|
+
let index2 = start;
|
|
10017
|
+
while (index2 < lines.length) {
|
|
10018
|
+
const line = lines[index2];
|
|
10019
|
+
if (indentof(line) !== indent || !/^\s*-\s?/.test(line)) break;
|
|
10020
|
+
const rest = line.slice(indent + 1).trim();
|
|
10021
|
+
if (rest !== "") {
|
|
10022
|
+
items.push(yamlscalarvalue(rest));
|
|
10023
|
+
index2 += 1;
|
|
10024
|
+
continue;
|
|
10025
|
+
}
|
|
10026
|
+
const nested = yamlblock(lines, index2 + 1, indent + 2);
|
|
10027
|
+
items.push(nested.value);
|
|
10028
|
+
index2 = nested.next;
|
|
10029
|
+
}
|
|
10030
|
+
return { value: items, next: index2 };
|
|
10031
|
+
}
|
|
10032
|
+
const mapping = {};
|
|
10033
|
+
let index = start;
|
|
10034
|
+
while (index < lines.length) {
|
|
10035
|
+
const line = lines[index];
|
|
10036
|
+
if (indentof(line) !== indent) break;
|
|
10037
|
+
const match = /^([A-Za-z][A-Za-z0-9]*):(?:\s(.*))?$/.exec(line.slice(indent));
|
|
10038
|
+
if (!match) break;
|
|
10039
|
+
const key = match[1];
|
|
10040
|
+
const rest = match[2];
|
|
10041
|
+
if (rest !== void 0 && rest !== "") {
|
|
10042
|
+
if (rest === "[]") {
|
|
10043
|
+
mapping[key] = [];
|
|
10044
|
+
index += 1;
|
|
10045
|
+
continue;
|
|
10046
|
+
}
|
|
10047
|
+
if (rest === "{}") {
|
|
10048
|
+
mapping[key] = {};
|
|
10049
|
+
index += 1;
|
|
10050
|
+
continue;
|
|
10051
|
+
}
|
|
10052
|
+
mapping[key] = yamlscalarvalue(rest);
|
|
10053
|
+
index += 1;
|
|
10054
|
+
continue;
|
|
10055
|
+
}
|
|
10056
|
+
const nested = yamlblock(lines, index + 1, indent + 2);
|
|
10057
|
+
mapping[key] = nested.value;
|
|
10058
|
+
index = nested.next;
|
|
10059
|
+
}
|
|
10060
|
+
if (index === start) throw new Error("The yaml workflow file left the documented subset.");
|
|
10061
|
+
return { value: mapping, next: index };
|
|
10062
|
+
}
|
|
10063
|
+
function yamlscalarvalue(text2) {
|
|
10064
|
+
if (text2.startsWith('"')) {
|
|
10065
|
+
const parsed = JSON.parse(text2);
|
|
10066
|
+
return typeof parsed === "string" ? parsed : text2;
|
|
10067
|
+
}
|
|
10068
|
+
if (text2 === "true") return true;
|
|
10069
|
+
if (text2 === "false") return false;
|
|
10070
|
+
if (text2 === "null") return null;
|
|
10071
|
+
if (/^-?\d+(?:\.\d+)?$/.test(text2)) return Number(text2);
|
|
10072
|
+
return text2;
|
|
10073
|
+
}
|
|
10074
|
+
|
|
10075
|
+
// mcpserver.ts
|
|
10076
|
+
var localhostbind = "127.0.0.1";
|
|
10077
|
+
var defaultmcpport = 7436;
|
|
10078
|
+
function rpcerrorof(code, message, data) {
|
|
10079
|
+
return { code, message, ...data !== void 0 ? { data } : {} };
|
|
10080
|
+
}
|
|
10081
|
+
function defaultmcpconfig() {
|
|
10082
|
+
return { port: defaultmcpport, transports: ["stdio", "http"], enabled: false };
|
|
10083
|
+
}
|
|
10084
|
+
function unwraphttppost(value) {
|
|
10085
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
10086
|
+
const candidate = value;
|
|
10087
|
+
if (candidate.transport === "http" && candidate.frame && typeof candidate.frame === "object" && !Array.isArray(candidate.frame)) return candidate.frame;
|
|
10088
|
+
}
|
|
10089
|
+
return value;
|
|
10090
|
+
}
|
|
10091
|
+
function parseframe(raw) {
|
|
10092
|
+
const parsed = unwraphttppost(JSON.parse(raw));
|
|
10093
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("A json rpc frame must be an object.");
|
|
10094
|
+
return parsed;
|
|
10095
|
+
}
|
|
10096
|
+
function serializeframe(frame) {
|
|
10097
|
+
return JSON.stringify(frame);
|
|
10098
|
+
}
|
|
10099
|
+
function validateframe(frame, methods, config) {
|
|
10100
|
+
if (frame.jsonrpc !== "2.0") return rpcerrorof("parse", "The frame must carry the jsonrpc 2.0 tag.");
|
|
10101
|
+
if (frame.id !== void 0 && typeof frame.id !== "number" && typeof frame.id !== "string" && frame.id !== null) return rpcerrorof("parse", "The frame id must be a number, a string or null.");
|
|
10102
|
+
if (frame.method === void 0 || frame.method.trim() === "") return rpcerrorof("method", "The frame carries no method to route.");
|
|
10103
|
+
if (!methods.some((entry) => entry.method === frame.method)) return rpcerrorof("method", `The server routes no method named ${frame.method}.`);
|
|
10104
|
+
if (frame.params !== void 0 && (typeof frame.params !== "object" || Array.isArray(frame.params))) return rpcerrorof("params", "The frame params must be an object.");
|
|
10105
|
+
if (config?.framesize !== void 0 && serializeframe(frame).length > config.framesize) return rpcerrorof("params", `The serialized frame exceeds the user configured frame size of ${config.framesize} characters.`);
|
|
10106
|
+
return void 0;
|
|
10107
|
+
}
|
|
10108
|
+
function respond(input) {
|
|
10109
|
+
return { jsonrpc: "2.0", ...input.id === void 0 ? input.error !== void 0 ? { id: null } : {} : { id: input.id }, ...input.error !== void 0 ? { error: input.error } : { result: input.result } };
|
|
10110
|
+
}
|
|
10111
|
+
function servermethods() {
|
|
10112
|
+
return [
|
|
10113
|
+
{ method: "initialize", handler: "initialize", description: "Completes the mcp handshake and returns the server info." },
|
|
10114
|
+
{ method: "ping", handler: "ping", description: "Answers keepalive frames with pong." },
|
|
10115
|
+
{ method: "tools/list", handler: "listtools", description: "Returns every tool with its version and json schema inputs." },
|
|
10116
|
+
{ method: "negotiate", handler: "negotiate", description: "Exchanges capability sets with the client." },
|
|
10117
|
+
{ method: "tools/call", handler: "dispatch", description: "Invokes one tool behind the consent gates." }
|
|
10118
|
+
];
|
|
10119
|
+
}
|
|
10120
|
+
function servercapabilities(input) {
|
|
10121
|
+
return { protocolversion, name: "devthink", version: protocolversion, toolversion: input.catalog.version, tools: alltools(input.catalog).length, namespaces: toolnamespaces, transports: input.config.transports };
|
|
10122
|
+
}
|
|
10123
|
+
function initialize(input) {
|
|
10124
|
+
void input.params;
|
|
10125
|
+
return { serverinfo: servercapabilities({ config: input.config, catalog: input.catalog }), protocolversion, instructions: "Devthink serves browser tools behind the human review gates: read only tools run once a session is approved while every tool with side effects executes exactly the approved plan step it names. No endpoint, provider or key is hardcoded; the user pairs every client." };
|
|
10126
|
+
}
|
|
10127
|
+
function ping(input) {
|
|
10128
|
+
return { pong: true, at: input.now };
|
|
10129
|
+
}
|
|
10130
|
+
function listtools(catalog) {
|
|
10131
|
+
return { tools: alltools(catalog).map((tool) => ({ name: tool.name, version: tool.version, description: tool.description, inputschema: tool.inputschema, risk: tool.risk, ...tool.consentmeta !== void 0 ? { consentmeta: tool.consentmeta.review } : {} })) };
|
|
10132
|
+
}
|
|
10133
|
+
function negotiate(input) {
|
|
10134
|
+
const client = input.client;
|
|
10135
|
+
if (client?.protocolversion !== void 0 && client.protocolversion !== input.server.protocolversion) return { agreed: false, mismatch: `The client speaks protocol version ${String(client.protocolversion)} while the server offers ${input.server.protocolversion}.` };
|
|
10136
|
+
if (client?.toolversion !== void 0 && client.toolversion > input.server.toolversion) return { agreed: false, mismatch: `The client requires tool version ${String(client.toolversion)} while the server offers ${String(input.server.toolversion)}.` };
|
|
10137
|
+
if (client?.transports !== void 0 && client.transports.some((transport) => !input.server.transports.includes(transport))) return { agreed: false, mismatch: "The client requires a transport the server configuration does not allow." };
|
|
10138
|
+
return { agreed: true, capabilities: input.server };
|
|
10139
|
+
}
|
|
10140
|
+
function connectclient(input) {
|
|
10141
|
+
return { id: input.id ?? `client-${input.now}`, transport: input.transport, paired: false, connectedat: input.now };
|
|
10142
|
+
}
|
|
10143
|
+
function disconnectclient(clients, id, now) {
|
|
10144
|
+
return clients.map((client) => client.id === id && client.disconnectedat === void 0 ? { ...client, disconnectedat: now } : client);
|
|
10145
|
+
}
|
|
10146
|
+
async function dispatchtool(input) {
|
|
10147
|
+
const params = input.params;
|
|
10148
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) return { error: rpcerrorof("params", "The tool call needs its params object.") };
|
|
10149
|
+
if (typeof params.name !== "string" || !params.name.trim()) return { error: rpcerrorof("params", "The tool call needs the namespaced name of the tool it invokes.") };
|
|
10150
|
+
const tool = resolvetool(input.catalog, params.name.trim());
|
|
10151
|
+
if (tool === void 0) return { error: rpcerrorof("params", `The catalog holds no unambiguous tool named ${params.name.trim()}.`) };
|
|
10152
|
+
const floor = input.client.capabilities?.toolversion ?? input.catalog.version;
|
|
10153
|
+
if (tool.version < floor) return { error: rpcerrorof("params", `The tool ${tool.name} of version ${tool.version} stays below the negotiated compatibility floor of ${floor}.`) };
|
|
10154
|
+
const stepid = typeof params.stepid === "string" ? params.stepid : void 0;
|
|
10155
|
+
const gate = tooldispatchgate({ client: input.client, tool, session: input.session, plan: input.plan, origin: input.origin, ...stepid !== void 0 ? { stepid } : {}, now: input.now });
|
|
10156
|
+
if (!gate.allowed) return { error: rpcerrorof("consentrefused", gate.reason ?? "The consent gates refused the tool call.") };
|
|
10157
|
+
const step = tool.risk === "read" ? { id: `mcp-${input.client.id}-${input.now}`, kind: tool.kind, summary: tool.description.split(".")[0] ?? tool.description, risk: "read", ...typeof params.target === "string" ? { target: params.target } : {}, ...typeof params.value === "string" ? { value: params.value } : {}, ...params.options !== void 0 && typeof params.options === "object" && !Array.isArray(params.options) ? { options: JSON.stringify(params.options) } : {} } : input.plan?.steps.find((candidate) => candidate.id === stepid);
|
|
10158
|
+
if (step === void 0) return { error: rpcerrorof("consentrefused", "The tool call names a step the approved plan does not carry.") };
|
|
10159
|
+
try {
|
|
10160
|
+
const result = await input.execute(step);
|
|
10161
|
+
return { result, step };
|
|
10162
|
+
} catch (error) {
|
|
10163
|
+
return { error: rpcerrorof("internal", error instanceof Error ? error.message : String(error)) };
|
|
10164
|
+
}
|
|
10165
|
+
}
|
|
10166
|
+
async function handleframe(input) {
|
|
10167
|
+
if (input.raw !== void 0 && input.config.framesize !== void 0 && input.raw.length > input.config.framesize) return respond({ id: null, error: rpcerrorof("params", `The wire frame exceeds the user configured frame size of ${input.config.framesize} characters.`) });
|
|
10168
|
+
let frame;
|
|
10169
|
+
if (input.raw !== void 0) {
|
|
10170
|
+
try {
|
|
10171
|
+
frame = parseframe(input.raw);
|
|
10172
|
+
} catch {
|
|
10173
|
+
return respond({ id: null, error: rpcerrorof("parse", "The wire frame does not parse as json.") });
|
|
10174
|
+
}
|
|
10175
|
+
} else if (input.frame !== void 0) {
|
|
10176
|
+
frame = input.frame;
|
|
10177
|
+
} else {
|
|
10178
|
+
return respond({ id: null, error: rpcerrorof("parse", "The server received no frame to route.") });
|
|
10179
|
+
}
|
|
10180
|
+
const invalid = validateframe(frame, servermethods(), input.config);
|
|
10181
|
+
if (invalid !== void 0) return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, error: invalid });
|
|
10182
|
+
const entry = servermethods().find((candidate) => candidate.method === frame.method);
|
|
10183
|
+
if (entry === void 0) return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, error: rpcerrorof("method", `The server routes no method named ${String(frame.method)}.`) });
|
|
10184
|
+
const params = frame.params;
|
|
10185
|
+
if (entry.handler === "initialize") return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, result: initialize({ ...params !== void 0 ? { params } : {}, config: input.config, catalog: input.catalog }) });
|
|
10186
|
+
if (entry.handler === "ping") return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, result: ping({ now: input.now }) });
|
|
10187
|
+
if (entry.handler === "listtools") return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, result: listtools(input.catalog) });
|
|
10188
|
+
if (entry.handler === "negotiate") {
|
|
10189
|
+
const server = servercapabilities({ config: input.config, catalog: input.catalog });
|
|
10190
|
+
const clientcaps = params?.capabilities && typeof params.capabilities === "object" && !Array.isArray(params.capabilities) ? params.capabilities : void 0;
|
|
10191
|
+
const outcome = negotiate({ ...clientcaps !== void 0 ? { client: clientcaps } : {}, server });
|
|
10192
|
+
return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, ...outcome.agreed ? { result: outcome.capabilities } : { error: rpcerrorof("params", outcome.mismatch ?? "The capability negotiation did not agree.") } });
|
|
10193
|
+
}
|
|
10194
|
+
const dispatched = await dispatchtool({ ...params !== void 0 ? { params } : {}, client: input.client, catalog: input.catalog, ...input.session !== void 0 ? { session: input.session } : {}, ...input.plan !== void 0 ? { plan: input.plan } : {}, origin: input.origin, now: input.now, execute: input.execute });
|
|
10195
|
+
return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, ...dispatched.error !== void 0 ? { error: dispatched.error } : { result: dispatched.result } });
|
|
10196
|
+
}
|
|
10197
|
+
function bindlocalhost(config) {
|
|
10198
|
+
const bind = config.bind !== void 0 && config.bind.trim() !== "" ? config.bind.trim() : localhostbind;
|
|
10199
|
+
return { bind, port: config.port, localhost: bind === localhostbind || bind === "localhost" || bind === "::1" };
|
|
10200
|
+
}
|
|
10201
|
+
function launchbridge(input) {
|
|
10202
|
+
return { id: input.id ?? `bridge-${input.now}`, host: input.host, connected: true, ...input.pid !== void 0 ? { pid: input.pid } : {}, startedat: input.now, restarts: 0, received: 0, sent: 0 };
|
|
10203
|
+
}
|
|
10204
|
+
function relayframe(input) {
|
|
10205
|
+
return { ...input.bridge, connected: true, received: input.bridge.received + (input.direction === "inbound" ? 1 : 0), sent: input.bridge.sent + (input.direction === "outbound" ? 1 : 0), lastframeat: input.now };
|
|
10206
|
+
}
|
|
10207
|
+
function toolcallevent(input) {
|
|
10208
|
+
return { id: input.id, clientid: input.clientid, tool: input.tool, origin: input.origin, ok: input.ok, ...input.code !== void 0 ? { code: input.code } : {}, at: input.now };
|
|
10209
|
+
}
|
|
10210
|
+
|
|
9104
10211
|
// extension/pagesession.ts
|
|
9105
10212
|
function capturepagestate(sections) {
|
|
9106
10213
|
const wants = (section) => sections.includes(section);
|
|
@@ -14853,12 +15960,24 @@ function workflowstepofentry(value) {
|
|
|
14853
15960
|
async function executeworkflowrun(step, session, plan, tabid2, origin, dry) {
|
|
14854
15961
|
const options = stepoptions2(step);
|
|
14855
15962
|
const workflowid = typeof options.workflowid === "string" ? options.workflowid : "";
|
|
14856
|
-
const
|
|
14857
|
-
if (!
|
|
14858
|
-
for (const workfloworigin of
|
|
15963
|
+
const storedrecord = await memory.getworkflowrecord(workflowid);
|
|
15964
|
+
if (!storedrecord) throw new Error(`No composed workflow matches ${workflowid || "the reviewed id"}.`);
|
|
15965
|
+
for (const workfloworigin of storedrecord.origins) {
|
|
14859
15966
|
if (!origingranted(session, workfloworigin)) throw new Error(`The workflow origin ${workfloworigin} falls outside the session grants.`);
|
|
14860
15967
|
}
|
|
14861
|
-
const
|
|
15968
|
+
const reviewgate = runreviewgranted(storedrecord);
|
|
15969
|
+
if (!reviewgate.allowed) throw new Error(reviewgate.reason ?? "The workflow stays unreviewed until its import or rollback review approves the expanded step list.");
|
|
15970
|
+
let record2 = storedrecord;
|
|
15971
|
+
for (const override of await memory.listsiteoverrides(storedrecord.id)) {
|
|
15972
|
+
try {
|
|
15973
|
+
record2 = applyoverride(record2, override);
|
|
15974
|
+
await audit("workflow", `Applied the per site override ${override.id} of the pattern ${override.pattern} to the workflow ${record2.name}: ${Object.entries(override.deltas).map(([knob, delta]) => `${knob} ${delta}`).join(", ") || "no delta"}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
15975
|
+
} catch (error) {
|
|
15976
|
+
await audit("workflow", `Skipped the per site override ${override.id} of the pattern ${override.pattern} for the workflow ${storedrecord.name}: ${error instanceof Error ? error.message : String(error)}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
15977
|
+
}
|
|
15978
|
+
}
|
|
15979
|
+
const backgroundflag = (await memory.getbackgroundruns())[record2.id] === true || options.background === true;
|
|
15980
|
+
const run = { ...newworkflowrun({ workflowid: record2.id, dryrun: dry, now: Date.now() }), ...backgroundflag === true && !dry ? { background: true } : {} };
|
|
14862
15981
|
await memory.setworkflowrun(run);
|
|
14863
15982
|
await memory.setrunscopes(run.id, runscopes(options.variables));
|
|
14864
15983
|
if (dry) {
|
|
@@ -14870,6 +15989,30 @@ async function executeworkflowrun(step, session, plan, tabid2, origin, dry) {
|
|
|
14870
15989
|
await audit("workflow", `The dry run of the workflow ${record2.name} evaluated ${evaluated.log.length} step${evaluated.log.length === 1 ? "" : "s"} read only; ${refused} step${refused === 1 ? "" : "s"} refused for lacking a read only projection and nothing was mutated.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
14871
15990
|
return { ok: true, summary: `The dry run evaluated ${evaluated.log.length} steps read only; ${refused} refused for lacking a read only projection.`, details: { runid: run.id, state: evaluated.run.state, dryrun: true, executed: evaluated.log.length - refused, refused, total: record2.steps.length } };
|
|
14872
15991
|
}
|
|
15992
|
+
if (run.background === true) {
|
|
15993
|
+
await audit("workflow", `Started the background run ${run.id} of the workflow ${record2.name} version ${record2.version} with ${record2.steps.length} reviewed steps; the panel may close, every step checkpoints and the worker wake restores the run through the same gates.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
15994
|
+
void performworkflowrun({ record: record2, run, step, session, plan, tabid: tabid2, origin, options, dry }).catch(async (error) => {
|
|
15995
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
15996
|
+
await memory.setworkflowrun({ ...run, state: "failed", endedat: Date.now(), failreason: reason });
|
|
15997
|
+
await memory.addrunhistory({ runid: run.id, workflowid: record2.id, outcome: "failed", steps: run.cursor, total: record2.steps.length, duration: Date.now() - run.startedat, cause: runcauseof(options), startedat: run.startedat, endedat: Date.now() });
|
|
15998
|
+
await audit("error", `The background run ${run.id} of the workflow ${record2.name} failed: ${reason}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
15999
|
+
await refreshbadge().catch(() => {
|
|
16000
|
+
});
|
|
16001
|
+
});
|
|
16002
|
+
return { ok: true, summary: `The workflow run ${run.id} started in the background with ${record2.steps.length} reviewed steps and the panel may close; the checkpoints restore it on every worker wake.`, details: { runid: run.id, state: "running", background: true, executed: 0, total: record2.steps.length } };
|
|
16003
|
+
}
|
|
16004
|
+
return await performworkflowrun({ record: record2, run, step, session, plan, tabid: tabid2, origin, options, dry });
|
|
16005
|
+
}
|
|
16006
|
+
function runcauseof(options) {
|
|
16007
|
+
const variables = options.variables;
|
|
16008
|
+
if (variables !== null && typeof variables === "object" && !Array.isArray(variables)) {
|
|
16009
|
+
const cause = variables.triggercause;
|
|
16010
|
+
if (typeof cause === "string" && cause.trim() !== "") return cause;
|
|
16011
|
+
}
|
|
16012
|
+
return "manual";
|
|
16013
|
+
}
|
|
16014
|
+
async function performworkflowrun(input) {
|
|
16015
|
+
const { record: record2, run, step, session, plan, tabid: tabid2, origin, options, dry } = input;
|
|
14873
16016
|
const guards = { cancelled: false };
|
|
14874
16017
|
activeworkflowruns.set(run.id, guards);
|
|
14875
16018
|
await audit("workflow", `Started the run ${run.id} of the workflow ${record2.name} version ${record2.version} with ${record2.steps.length} reviewed steps; every step passes the session, plan review and origin gates.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
@@ -14951,6 +16094,8 @@ async function executeworkflowrun(step, session, plan, tabid2, origin, dry) {
|
|
|
14951
16094
|
}
|
|
14952
16095
|
const executed = result.run.cursor;
|
|
14953
16096
|
await memory.setprogress(recordworkflow(await memory.getprogress(), plan.id, step.id, { family: "run", detail: `Ran the workflow ${record2.name}`, runid: run.id, executed, total: record2.steps.length }, Date.now()));
|
|
16097
|
+
const endedat = result.run.endedat ?? Date.now();
|
|
16098
|
+
await memory.addrunhistory({ runid: run.id, workflowid: record2.id, outcome: result.run.state, steps: executed, total: record2.steps.length, duration: Math.max(0, endedat - run.startedat), cause: runcauseof(options), startedat: run.startedat, endedat, ...dry === true ? { dryrun: true } : {} });
|
|
14954
16099
|
await audit("workflow", `The run ${run.id} of the workflow ${record2.name} ended ${result.run.state} after ${executed} of ${record2.steps.length} steps${result.run.failreason !== void 0 ? ` with the failure ${result.run.failreason}` : ""}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
14955
16100
|
await refreshbadge();
|
|
14956
16101
|
return { ok: result.run.state === "done", summary: `The workflow run ended ${result.run.state} after ${executed} of ${record2.steps.length} steps.`, details: { runid: run.id, state: result.run.state, executed, total: record2.steps.length, ...result.run.failreason !== void 0 ? { failreason: result.run.failreason } : {} } };
|
|
@@ -15252,6 +16397,8 @@ async function executeaction(step, session, plan, tabid2, origin, settings, verd
|
|
|
15252
16397
|
} else if (istriggeraction(step.kind)) {
|
|
15253
16398
|
if (!session || !plan || plan.state !== "approved") throw new Error("Trigger kinds refuse to run outside an approved session plan.");
|
|
15254
16399
|
output = await executetriggerstep(step, session, plan, tabid2, origin);
|
|
16400
|
+
} else if (step.kind === "listruns") {
|
|
16401
|
+
output = await executelistruns(step, session);
|
|
15255
16402
|
} else {
|
|
15256
16403
|
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
15257
16404
|
const fresh = await snapshot(tabid2);
|
|
@@ -15506,7 +16653,7 @@ async function handlerequest(message, sender) {
|
|
|
15506
16653
|
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
15507
16654
|
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
15508
16655
|
const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
|
|
15509
|
-
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks, captures: capturemetadata, capturepairs, capturepolicy: runsettings?.capturepolicy ?? "manual", media: mediarecords, imagebatches, recordingconsents, recordingactive: [...activerecordings.values()].map((active) => ({ id: active.record.id, kind: active.record.kind, scope: active.record.scope, startedat: active.record.startedat, stopat: active.stopat })), recordingwindow: runsettings?.recordingwindow, calls, endpoints, fetchconsents, apikeys, callretention: runsettings?.callretention, fetchesactive: activefetches.size, exchanges, channels, subscriptions, apimap, messages: messagecount, webrequestgrant: runsettings?.webrequestgrant === true, bodyretention: runsettings?.bodyretention, timelineretention: runsettings?.timelineretention, timeline, consoleconsents: await memory.getconsoleconsents(), rotationtargets: await memory.getrotationtargets(), levelsummaries: await memory.getlevelsummaries(), cdpsessions: await memory.getcdpsessions(), cdpcommands: await memory.getcdpcommands(), cdpeventrules: await memory.getcdpeventrules(), breakpoints: await memory.getbreakpoints(), pauses: await memory.getpauses(), watchexpressions: await memory.getwatchexpressions(), scriptoverrides: await memory.getscriptoverrides(), debuggergrants: await memory.getdebuggergrants(), pauseretention: runsettings?.pauseretention, breakpointceiling: runsettings?.breakpointceiling, cdpattached: [...activecdpsessions.values()].filter((active) => active.session.detachedat === void 0).length, profileretention: runsettings?.profileretention, traceceiling: runsettings?.traceceiling, profile: profilereport({ flows: await memory.getflowmetrics(), heaps: await memory.getheaprecords(), samples: await memory.getgrowsamples(), trends: await memory.gettrends(), profiles: await memory.getcpuprofiles(), shifts: await memory.getshiftentries(), traces: await memory.gettracerecords(), sourcemaps: await memory.getsourcemaps(), consents: await memory.getsourcemapconsents() }), profileactive: activememorytrackers.size + activeprofiletargets.size, profiletargets: [...activeprofiletargets.values()].flatMap((entry) => entry.targets), socketsactive: activesockets.size, emulation: emulationreport({ ...plan && await loademulationstate(plan.id) !== void 0 ? { state: await loademulationstate(plan.id) } : {}, devices: await memory.getdevicepresets(), networks: await memory.getnetworkpresets(), locations: await memory.getlocationpresets(), agents: await memory.getagentpresets(), blackbox: await memory.getblackboxrules(), permissions: await memory.getpermissionoverrides(), consents: await memory.getlocationconsents() }), emulatedlayers: plan ? layernames(await loademulationstate(plan.id)) : [], emulationretention: runsettings?.emulationretention, traffic, tokens, authflows, activerules: [...activerules.values()].reduce((total, ruleset) => total + ruleset.blocks.filter((rule) => rule.revertedat === void 0).length + ruleset.mocks.filter((rule) => rule.revertedat === void 0).length + ruleset.rewrites.filter((rule) => rule.revertedat === void 0).length + (ruleset.proxy !== void 0 && ruleset.proxy.revertedat === void 0 ? 1 : 0), 0), sessionmemory: sessionreport({ records: sessionrecords, events: await memory.getsessionevents(), folders: await memory.getsessionfolders(), diffs: await memory.getsessiondiffs(), ...autosnapshot !== void 0 ? { auto: autosnapshot.interval } : {}, ...crashed ? { crashed: true } : {} }), autosnapshotstate: autosnapshot, sessionretention: runsettings?.sessionretention, trigger: triggerlist({ rules: await memory.gettriggerules(), workflows: await memory.listworkflows(), queue: await memory.gettriggerqueue() }), triggerretention: runsettings?.triggerretention, workflow: workflowreport({ workflows: await memory.listworkflows(), runs: await memory.listworkflowruns(), templates: await memory.getsteptemplates(), ...newestworkflowrun !== void 0 ? { log: await memory.getrunlog(newestworkflowrun.id), scopes: await memory.getrunscopes(newestworkflowrun.id), provenance: await memory.getworkflowprovenance(newestworkflowrun.id), control: await memory.listcontroldecisions(newestworkflowrun.id) } : {} }), runlogretention: runsettings?.runlogretention, ...taskstate !== void 0 ? { taskstate } : {}, ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
|
|
16656
|
+
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks, captures: capturemetadata, capturepairs, capturepolicy: runsettings?.capturepolicy ?? "manual", media: mediarecords, imagebatches, recordingconsents, recordingactive: [...activerecordings.values()].map((active) => ({ id: active.record.id, kind: active.record.kind, scope: active.record.scope, startedat: active.record.startedat, stopat: active.stopat })), recordingwindow: runsettings?.recordingwindow, calls, endpoints, fetchconsents, apikeys, callretention: runsettings?.callretention, fetchesactive: activefetches.size, exchanges, channels, subscriptions, apimap, messages: messagecount, webrequestgrant: runsettings?.webrequestgrant === true, bodyretention: runsettings?.bodyretention, timelineretention: runsettings?.timelineretention, timeline, consoleconsents: await memory.getconsoleconsents(), rotationtargets: await memory.getrotationtargets(), levelsummaries: await memory.getlevelsummaries(), cdpsessions: await memory.getcdpsessions(), cdpcommands: await memory.getcdpcommands(), cdpeventrules: await memory.getcdpeventrules(), breakpoints: await memory.getbreakpoints(), pauses: await memory.getpauses(), watchexpressions: await memory.getwatchexpressions(), scriptoverrides: await memory.getscriptoverrides(), debuggergrants: await memory.getdebuggergrants(), pauseretention: runsettings?.pauseretention, breakpointceiling: runsettings?.breakpointceiling, cdpattached: [...activecdpsessions.values()].filter((active) => active.session.detachedat === void 0).length, profileretention: runsettings?.profileretention, traceceiling: runsettings?.traceceiling, profile: profilereport({ flows: await memory.getflowmetrics(), heaps: await memory.getheaprecords(), samples: await memory.getgrowsamples(), trends: await memory.gettrends(), profiles: await memory.getcpuprofiles(), shifts: await memory.getshiftentries(), traces: await memory.gettracerecords(), sourcemaps: await memory.getsourcemaps(), consents: await memory.getsourcemapconsents() }), profileactive: activememorytrackers.size + activeprofiletargets.size, profiletargets: [...activeprofiletargets.values()].flatMap((entry) => entry.targets), socketsactive: activesockets.size, emulation: emulationreport({ ...plan && await loademulationstate(plan.id) !== void 0 ? { state: await loademulationstate(plan.id) } : {}, devices: await memory.getdevicepresets(), networks: await memory.getnetworkpresets(), locations: await memory.getlocationpresets(), agents: await memory.getagentpresets(), blackbox: await memory.getblackboxrules(), permissions: await memory.getpermissionoverrides(), consents: await memory.getlocationconsents() }), emulatedlayers: plan ? layernames(await loademulationstate(plan.id)) : [], emulationretention: runsettings?.emulationretention, traffic, tokens, authflows, activerules: [...activerules.values()].reduce((total, ruleset) => total + ruleset.blocks.filter((rule) => rule.revertedat === void 0).length + ruleset.mocks.filter((rule) => rule.revertedat === void 0).length + ruleset.rewrites.filter((rule) => rule.revertedat === void 0).length + (ruleset.proxy !== void 0 && ruleset.proxy.revertedat === void 0 ? 1 : 0), 0), sessionmemory: sessionreport({ records: sessionrecords, events: await memory.getsessionevents(), folders: await memory.getsessionfolders(), diffs: await memory.getsessiondiffs(), ...autosnapshot !== void 0 ? { auto: autosnapshot.interval } : {}, ...crashed ? { crashed: true } : {} }), autosnapshotstate: autosnapshot, sessionretention: runsettings?.sessionretention, trigger: triggerlist({ rules: await memory.gettriggerules(), workflows: await memory.listworkflows(), queue: await memory.gettriggerqueue() }), triggerretention: runsettings?.triggerretention, workflow: workflowreport({ workflows: await memory.listworkflows(), runs: await memory.listworkflowruns(), templates: await memory.getsteptemplates(), ...newestworkflowrun !== void 0 ? { log: await memory.getrunlog(newestworkflowrun.id), scopes: await memory.getrunscopes(newestworkflowrun.id), provenance: await memory.getworkflowprovenance(newestworkflowrun.id), control: await memory.listcontroldecisions(newestworkflowrun.id) } : {} }), runlogretention: runsettings?.runlogretention, runhistoryretention: runsettings?.runhistoryretention, editor: editorstate({ versions: await memory.listworkflowversions(), diffs: await memory.listversiondiffs(), history: await memory.gethistory(), overrides: await memory.listsiteoverrides(), imports: (await memory.listworkflowimports()).map((entry) => ({ id: entry.id, workflowid: entry.record.id, name: entry.record.name, version: entry.record.version, steps: entry.record.steps.length, risk: entry.record.risk, importedat: entry.importedat, ...entry.filename !== void 0 ? { filename: entry.filename } : {} })), backgroundruns: await memory.getbackgroundruns(), watchdog: { ...runsettings?.watchdog !== void 0 ? { config: runsettings.watchdog } : {}, events: await memory.listwatchdogevents() } }), ...taskstate !== void 0 ? { taskstate } : {}, ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {}, mcp: await mcpstateof() };
|
|
15510
16657
|
}
|
|
15511
16658
|
case "capabilities":
|
|
15512
16659
|
return refreshcapabilities();
|
|
@@ -16728,8 +17875,13 @@ async function handlerequest(message, sender) {
|
|
|
16728
17875
|
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Workflow runs need an active browser session behind the consent gates.");
|
|
16729
17876
|
const record2 = await memory.getworkflowrecord(inputapprove.workflowid?.trim() ?? "");
|
|
16730
17877
|
if (!record2) throw new Error(`No composed workflow matches ${inputapprove.workflowid ?? ""}.`);
|
|
16731
|
-
|
|
16732
|
-
|
|
17878
|
+
if (record2.reviewstate === "pending") {
|
|
17879
|
+
await memory.addworkflowrecord({ ...record2, reviewstate: "approved" });
|
|
17880
|
+
await memory.removeworkflowversion(record2.id, record2.version).catch(() => {
|
|
17881
|
+
});
|
|
17882
|
+
}
|
|
17883
|
+
await audit("workflow", `The user approved the run review of the workflow ${record2.name} version ${record2.version} with its ${record2.steps.length} expanded step${record2.steps.length === 1 ? "" : "s"} shown${record2.reviewstate === "pending" ? "; the pending import or rollback review cleared and the workflow may run again" : ""}; the run still passes every consent gate per step.`, { sessionid: session.id });
|
|
17884
|
+
return { approved: true, steps: record2.steps.length, risk: record2.risk, ...record2.reviewstate === "pending" ? { reviewstate: "approved" } : {} };
|
|
16733
17885
|
}
|
|
16734
17886
|
case "executeworkflowstep": {
|
|
16735
17887
|
const inputsingle = message;
|
|
@@ -16775,8 +17927,8 @@ async function handlerequest(message, sender) {
|
|
|
16775
17927
|
const guards = activeworkflowruns.get(stored.id);
|
|
16776
17928
|
if (guards) guards.cancelled = true;
|
|
16777
17929
|
const paused = pauserun(stored, Date.now());
|
|
16778
|
-
await memory.setworkflowrun(paused);
|
|
16779
|
-
await audit("workflow", `Paused the workflow run ${paused.id} at the checkpoint of step cursor ${paused.cursor}; the resume continues exactly there.`, {});
|
|
17930
|
+
await memory.setworkflowrun({ ...paused, pausekind: "user" });
|
|
17931
|
+
await audit("workflow", `Paused the workflow run ${paused.id} at the checkpoint of step cursor ${paused.cursor}; the resume continues exactly there and the worker wake never auto-resumes a user pause.`, {});
|
|
16780
17932
|
await refreshbadge();
|
|
16781
17933
|
return { runid: paused.id, state: paused.state, cursor: paused.cursor };
|
|
16782
17934
|
}
|
|
@@ -16801,6 +17953,27 @@ async function handlerequest(message, sender) {
|
|
|
16801
17953
|
}
|
|
16802
17954
|
return await dispatchworkflowstep(dispatched, { session, plan, tabid: tab.id, origin });
|
|
16803
17955
|
};
|
|
17956
|
+
const debugbreakpoints = Array.isArray(message.breakpoints) ? message.breakpoints.filter((id) => typeof id === "string") : void 0;
|
|
17957
|
+
if (debugbreakpoints !== void 0) {
|
|
17958
|
+
const startcursor = stored.run.cursor;
|
|
17959
|
+
let segment = runtobreakpoint({ record: record2, cursor: startcursor, breakpoints: debugbreakpoints });
|
|
17960
|
+
if (segment.until === startcursor && startcursor < record2.steps.length) segment = runtobreakpoint({ record: record2, cursor: startcursor + 1, breakpoints: debugbreakpoints });
|
|
17961
|
+
if (segment.until === startcursor) {
|
|
17962
|
+
await memory.setworkflowrun({ ...stored.run, state: "paused", pausedat: Date.now(), cursor: startcursor });
|
|
17963
|
+
return { runid: stored.run.id, state: "paused", cursor: startcursor, ...segment.pausat !== void 0 ? { pausat: segment.pausat } : {}, remaining: segment.remaining };
|
|
17964
|
+
}
|
|
17965
|
+
const segmentresult = await runworkflowsegment({ record: record2, run: stored.run, startcursor, until: segment.until, session, plan, tabid: tab.id, origin, scopes, log, storedentries, execute: executeresume });
|
|
17966
|
+
if (segmentresult.failed !== void 0) {
|
|
17967
|
+
await memory.setworkflowrun({ ...stored.run, state: "failed", endedat: Date.now(), cursor: segmentresult.run.cursor, failreason: segmentresult.failed });
|
|
17968
|
+
await audit("workflow", `The debug resume of the run ${stored.run.id} failed at step cursor ${segmentresult.run.cursor}: ${segmentresult.failed}.`, { sessionid: session.id, planid: plan.id });
|
|
17969
|
+
await refreshbadge();
|
|
17970
|
+
return { runid: stored.run.id, state: "failed", cursor: segmentresult.run.cursor, failreason: segmentresult.failed };
|
|
17971
|
+
}
|
|
17972
|
+
await memory.setworkflowrun({ ...stored.run, state: "paused", pausedat: Date.now(), cursor: segment.until });
|
|
17973
|
+
await audit("workflow", `The debug resume of the run ${stored.run.id} ran ${segment.until - startcursor} step${segment.until - startcursor === 1 ? "" : "s"} and paused at the breakpoint ${segment.pausat ?? "end"} of step cursor ${segment.until}; ${segment.remaining} step${segment.remaining === 1 ? "" : "s"} remain.`, { sessionid: session.id, planid: plan.id });
|
|
17974
|
+
await refreshbadge();
|
|
17975
|
+
return { runid: stored.run.id, state: "paused", cursor: segment.until, ...segment.pausat !== void 0 ? { pausat: segment.pausat } : {}, remaining: segment.remaining };
|
|
17976
|
+
}
|
|
16804
17977
|
const resumed = await runworkflow({ record: record2, run: stored.run, scopes, log, execute: executeresume, now: Date.now(), gates: { sessionactive: Boolean(session && !session.stoppedat && !session.pausedat && session.expiresat > Date.now()), planapproved: plan.state === "approved", origingranted: (workfloworigin) => origingranted(session, workfloworigin) }, oncheckpoint: async (state) => {
|
|
16805
17978
|
await memory.setworkflowrun(state.run);
|
|
16806
17979
|
for (const entry of state.log.slice(storedentries)) await memory.addrunlogentry(state.run.id, entry);
|
|
@@ -17018,10 +18191,583 @@ async function handlerequest(message, sender) {
|
|
|
17018
18191
|
const fires = await memory.listtriggerfires();
|
|
17019
18192
|
return triggerlist({ rules: await memory.gettriggerules(), workflows: await memory.listworkflows(), queue: await memory.gettriggerqueue() });
|
|
17020
18193
|
}
|
|
18194
|
+
case "mcpstate": {
|
|
18195
|
+
return mcpstateof();
|
|
18196
|
+
}
|
|
18197
|
+
case "mcpserverconfig": {
|
|
18198
|
+
const inputconfig = message;
|
|
18199
|
+
const current = await mcpconfigof();
|
|
18200
|
+
const transports = Array.isArray(inputconfig.transports) && inputconfig.transports.length > 0 ? [...new Set(inputconfig.transports.filter((transport) => transport === "stdio" || transport === "http"))] : current.transports;
|
|
18201
|
+
const config = {
|
|
18202
|
+
...inputconfig.bind !== void 0 ? { bind: inputconfig.bind } : current.bind !== void 0 ? { bind: current.bind } : {},
|
|
18203
|
+
port: typeof inputconfig.port === "number" && Number.isFinite(inputconfig.port) && inputconfig.port > 0 && inputconfig.port <= 65535 ? Math.floor(inputconfig.port) : current.port,
|
|
18204
|
+
transports,
|
|
18205
|
+
...inputconfig.framesize !== void 0 || current.framesize !== void 0 ? { framesize: typeof inputconfig.framesize === "number" ? inputconfig.framesize : current.framesize } : {},
|
|
18206
|
+
...inputconfig.queuedepth !== void 0 || current.queuedepth !== void 0 ? { queuedepth: typeof inputconfig.queuedepth === "number" ? inputconfig.queuedepth : current.queuedepth } : {},
|
|
18207
|
+
...inputconfig.callretention !== void 0 || current.callretention !== void 0 ? { callretention: typeof inputconfig.callretention === "number" ? inputconfig.callretention : current.callretention } : {},
|
|
18208
|
+
enabled: inputconfig.enabled === true || inputconfig.enabled === void 0 && current.enabled === true,
|
|
18209
|
+
...inputconfig.remote === true || inputconfig.remote === void 0 && current.remote === true ? { remote: true } : {}
|
|
18210
|
+
};
|
|
18211
|
+
const bindcheck = serverbindgate(config);
|
|
18212
|
+
if (!bindcheck.allowed) throw new Error(bindcheck.reason ?? "The server bind failed its gate.");
|
|
18213
|
+
await memory.setmcpconfig(config);
|
|
18214
|
+
const binding = bindlocalhost(config);
|
|
18215
|
+
await audit("protocol", `Configured the mcp server for the ${binding.bind} bind on port ${binding.port} with the transports ${config.transports.join(" and ")}${config.framesize !== void 0 ? `, a frame size of ${config.framesize} characters` : " and no frame size cap"}${config.queuedepth !== void 0 ? `, a queue depth of ${config.queuedepth}` : " and an unbounded queue"}; every value stays the user choice and the server ${config.enabled ? "stays enabled" : "stays disabled"}.`, {});
|
|
18216
|
+
return mcpstateof();
|
|
18217
|
+
}
|
|
18218
|
+
case "mcpserverstart": {
|
|
18219
|
+
const config = { ...await mcpconfigof(), enabled: true };
|
|
18220
|
+
const gate = serverenablementgate(config);
|
|
18221
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The mcp server failed its enablement gate.");
|
|
18222
|
+
await memory.setmcpconfig(config);
|
|
18223
|
+
const binding = bindlocalhost(config);
|
|
18224
|
+
const bridge = await trybridgelaunch(false);
|
|
18225
|
+
await memory.setmcpstate({ state: "running", startedat: Date.now(), ...bridge !== void 0 ? { bridge } : {} });
|
|
18226
|
+
await audit("protocol", `The user started the mcp server on ${binding.bind}:${binding.port} with the tool catalog of ${listtools(buildtoolcatalog()).tools.length} tools across the browser, workflow, memory and system namespaces; the localhost bind stays the default and every client waits for the pairing approval.`, {});
|
|
18227
|
+
return mcpstateof();
|
|
18228
|
+
}
|
|
18229
|
+
case "mcpserverstop": {
|
|
18230
|
+
const clients = await memory.getclients();
|
|
18231
|
+
const now = Date.now();
|
|
18232
|
+
for (const client of clients) {
|
|
18233
|
+
const updated = disconnectclient(clients, client.id, now).find((entry) => entry.id === client.id);
|
|
18234
|
+
if (updated) await memory.setclient(updated);
|
|
18235
|
+
}
|
|
18236
|
+
await memory.setmcpstate({ state: "stopped", stoppedat: now });
|
|
18237
|
+
await audit("protocol", `The user stopped the mcp server; ${clients.length} connected client${clients.length === 1 ? "" : "s"} disconnected and no tool call passes the gates until the next start.`, {});
|
|
18238
|
+
return mcpstateof();
|
|
18239
|
+
}
|
|
18240
|
+
case "mcpclientdecision": {
|
|
18241
|
+
const inputdecision = message;
|
|
18242
|
+
const clientid = inputdecision.clientid ?? "";
|
|
18243
|
+
const clients = await memory.getclients();
|
|
18244
|
+
const client = clients.find((entry) => entry.id === clientid);
|
|
18245
|
+
if (!client || client.disconnectedat !== void 0) throw new Error(`No connected mcp client matches ${clientid}.`);
|
|
18246
|
+
if (typeof inputdecision.approved !== "boolean") throw new Error("The client pairing decision needs the reviewed approved flag.");
|
|
18247
|
+
const updated = inputdecision.approved ? { ...client, paired: true, pairedat: Date.now() } : { ...client, paired: false, disconnectedat: Date.now() };
|
|
18248
|
+
await memory.setclient(updated);
|
|
18249
|
+
await audit("protocol", `The user ${inputdecision.approved ? "approved the pairing of" : "refused and disconnected"} the mcp client ${clientid} on the ${client.transport} transport${inputdecision.approved ? "; its tool calls now pass the same consent gates as the panels" : "; its tool calls stay refused"}.`, {});
|
|
18250
|
+
return mcpstateof();
|
|
18251
|
+
}
|
|
18252
|
+
case "mcpclientdisconnect": {
|
|
18253
|
+
const inputdisconnect = message;
|
|
18254
|
+
const clientid = inputdisconnect.clientid ?? "";
|
|
18255
|
+
const clients = await memory.getclients();
|
|
18256
|
+
const client = clients.find((entry) => entry.id === clientid);
|
|
18257
|
+
if (!client || client.disconnectedat !== void 0) throw new Error(`No connected mcp client matches ${clientid}.`);
|
|
18258
|
+
await memory.setclient({ ...client, disconnectedat: Date.now() });
|
|
18259
|
+
await audit("protocol", `The user disconnected the mcp client ${clientid} on the ${client.transport} transport; its record stays for the audit trail.`, {});
|
|
18260
|
+
return mcpstateof();
|
|
18261
|
+
}
|
|
18262
|
+
case "mcpbridge": {
|
|
18263
|
+
const inputbridge = message;
|
|
18264
|
+
const state = await memory.getmcpstate();
|
|
18265
|
+
if (state?.state !== "running") throw new Error("The stdio bridge restart needs the running mcp server.");
|
|
18266
|
+
const bridge = await trybridgelaunch(true);
|
|
18267
|
+
await memory.setmcpstate({ ...state, ...bridge !== void 0 ? { bridge } : {} });
|
|
18268
|
+
await audit("protocol", bridge !== void 0 ? `The stdio bridge restarted the local client process on demand; the bridge record counts the restart.` : `The stdio bridge restart found no native messaging host because the browser exposes no native messaging permission under the current permission set; the mcp frame grammar and the panel path stay fully available.`, {});
|
|
18269
|
+
return mcpstateof();
|
|
18270
|
+
}
|
|
18271
|
+
case "mcpframe": {
|
|
18272
|
+
const inputframe = message;
|
|
18273
|
+
if (typeof inputframe.raw !== "string" || inputframe.raw.trim() === "") throw new Error("The mcp frame intake needs the raw wire frame.");
|
|
18274
|
+
const transport = inputframe.transport === "http" ? "http" : "stdio";
|
|
18275
|
+
return processmcpframe(inputframe.raw, inputframe.clientid ?? "", transport);
|
|
18276
|
+
}
|
|
18277
|
+
case "runtobreakpoint": {
|
|
18278
|
+
const inputdebug = message;
|
|
18279
|
+
const session = await memory.getsession();
|
|
18280
|
+
const plan = await memory.getplan();
|
|
18281
|
+
if (!session || session.stoppedat || session.pausedat || session.expiresat <= Date.now()) throw new Error("Debug runs need a live, unpaused browser session behind the consent gates.");
|
|
18282
|
+
if (!plan || plan.state !== "approved") throw new Error("Debug runs need the approved plan review.");
|
|
18283
|
+
const record2 = await memory.getworkflowrecord(inputdebug.workflowid?.trim() ?? "");
|
|
18284
|
+
if (!record2) throw new Error(`No composed workflow matches ${inputdebug.workflowid ?? ""}.`);
|
|
18285
|
+
for (const workfloworigin of record2.origins) {
|
|
18286
|
+
if (!origingranted(session, workfloworigin)) throw new Error(`The workflow origin ${workfloworigin} falls outside the session grants.`);
|
|
18287
|
+
}
|
|
18288
|
+
const reviewgate = runreviewgranted(record2);
|
|
18289
|
+
if (!reviewgate.allowed) throw new Error(reviewgate.reason ?? "The workflow stays unreviewed until its import or rollback review approves the expanded step list.");
|
|
18290
|
+
const marked = [.../* @__PURE__ */ new Set([...await memory.getworkflowbreakpoints(record2.id), ...record2.steps.filter((step) => step.breakpoint === true).map((step) => step.id)])];
|
|
18291
|
+
const segment = runtobreakpoint({ record: record2, cursor: 0, breakpoints: marked });
|
|
18292
|
+
if (segment.pausat === void 0) throw new Error("No breakpoint marks any step of the workflow; mark one on the canvas before the debug run starts.");
|
|
18293
|
+
const run = newworkflowrun({ workflowid: record2.id, now: Date.now() });
|
|
18294
|
+
await memory.setworkflowrun(run);
|
|
18295
|
+
await memory.setrunscopes(run.id, []);
|
|
18296
|
+
if (segment.until === 0) {
|
|
18297
|
+
await memory.setworkflowrun({ ...run, state: "paused", pausedat: Date.now(), cursor: 0 });
|
|
18298
|
+
await audit("workflow", `The debug run ${run.id} of the workflow ${record2.name} paused at the first breakpoint ${segment.pausat} before any step ran; ${record2.steps.length} step${record2.steps.length === 1 ? "" : "s"} remain.`, { sessionid: session.id, planid: plan.id });
|
|
18299
|
+
await refreshbadge();
|
|
18300
|
+
return { runid: run.id, state: "paused", cursor: 0, pausat: segment.pausat, remaining: record2.steps.length };
|
|
18301
|
+
}
|
|
18302
|
+
const { tab, origin } = await activecontext();
|
|
18303
|
+
const executedebug = async (dispatched, stepcontext) => {
|
|
18304
|
+
if (iscontrolflowkind(dispatched.kind)) {
|
|
18305
|
+
const controlled = await runcontrolstep({ step: dispatched, scopes: stepcontext.scopes, outputs: stepcontext.outputs ?? {}, execute: executedebug, now: Date.now(), runid: run.id, pagestate: await pagestateof(tab.id), resolveelements: async (selector) => await resolveelements(tab.id, selector) });
|
|
18306
|
+
return { ...controlled.output, scopes: controlled.scopes, log: controlled.log };
|
|
18307
|
+
}
|
|
18308
|
+
return await dispatchworkflowstep(dispatched, { session, plan, tabid: tab.id, origin });
|
|
18309
|
+
};
|
|
18310
|
+
const segmentresult = await runworkflowsegment({ record: record2, run, startcursor: 0, until: segment.until, session, plan, tabid: tab.id, origin, scopes: [{ name: "root", variables: [] }], log: [], storedentries: 0, execute: executedebug });
|
|
18311
|
+
if (segmentresult.failed !== void 0) {
|
|
18312
|
+
await memory.setworkflowrun({ ...run, state: "failed", endedat: Date.now(), cursor: segmentresult.run.cursor, failreason: segmentresult.failed });
|
|
18313
|
+
await memory.addrunhistory({ runid: run.id, workflowid: record2.id, outcome: "failed", steps: segmentresult.run.cursor, total: record2.steps.length, duration: Date.now() - run.startedat, cause: "debug", startedat: run.startedat, endedat: Date.now() });
|
|
18314
|
+
await audit("workflow", `The debug run ${run.id} of the workflow ${record2.name} failed at step cursor ${segmentresult.run.cursor} before the breakpoint ${segment.pausat}: ${segmentresult.failed}.`, { sessionid: session.id, planid: plan.id });
|
|
18315
|
+
await refreshbadge();
|
|
18316
|
+
return { runid: run.id, state: "failed", cursor: segmentresult.run.cursor, failreason: segmentresult.failed };
|
|
18317
|
+
}
|
|
18318
|
+
await memory.setworkflowrun({ ...run, state: "paused", pausedat: Date.now(), cursor: segment.until });
|
|
18319
|
+
await memory.addrunhistory({ runid: run.id, workflowid: record2.id, outcome: "paused", steps: segment.until, total: record2.steps.length, duration: Date.now() - run.startedat, cause: "debug", startedat: run.startedat, endedat: Date.now() });
|
|
18320
|
+
await audit("workflow", `The debug run ${run.id} of the workflow ${record2.name} ran ${segment.until} step${segment.until === 1 ? "" : "s"} and paused at the breakpoint ${segment.pausat}; ${segment.remaining} step${segment.remaining === 1 ? "" : "s"} remain and the resume continues exactly there.`, { sessionid: session.id, planid: plan.id });
|
|
18321
|
+
await refreshbadge();
|
|
18322
|
+
return { runid: run.id, state: "paused", cursor: segment.until, pausat: segment.pausat, remaining: segment.remaining };
|
|
18323
|
+
}
|
|
18324
|
+
case "editormodel": {
|
|
18325
|
+
const inputmodel = message;
|
|
18326
|
+
const record2 = await memory.getworkflowrecord(inputmodel.workflowid?.trim() ?? "");
|
|
18327
|
+
if (!record2) throw new Error(`No composed workflow matches ${inputmodel.workflowid ?? ""}.`);
|
|
18328
|
+
const layout = await memory.geteditorlayout(record2.id);
|
|
18329
|
+
const model = loadworkflow(record2, layout);
|
|
18330
|
+
const breakpoints = await memory.getworkflowbreakpoints(record2.id);
|
|
18331
|
+
const nodes = model.nodes.map((node) => node.step !== void 0 && breakpoints.includes(node.step.id) && node.step.breakpoint !== true ? { step: { ...node.step, breakpoint: true }, x: node.x, y: node.y } : node);
|
|
18332
|
+
return { model: { ...model, nodes }, record: record2, versions: await memory.listworkflowversions(record2.id), breakpoints };
|
|
18333
|
+
}
|
|
18334
|
+
case "editorsave": {
|
|
18335
|
+
const inputsave = message;
|
|
18336
|
+
const session = await memory.getsession();
|
|
18337
|
+
const plan = await memory.getplan();
|
|
18338
|
+
const model = inputsave.model;
|
|
18339
|
+
if (!model) throw new Error("The editor save needs the canvas model.");
|
|
18340
|
+
const gate = editorsavegate({ session, plan, model, now: Date.now() });
|
|
18341
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The editor save failed its consent gate.");
|
|
18342
|
+
const record2 = saveworkflow(model, { now: Date.now(), kindallowed: (kind) => {
|
|
18343
|
+
try {
|
|
18344
|
+
actionrisk(kind);
|
|
18345
|
+
return true;
|
|
18346
|
+
} catch {
|
|
18347
|
+
return false;
|
|
18348
|
+
}
|
|
18349
|
+
}, riskof: (kind) => actionrisk(kind) });
|
|
18350
|
+
await memory.addworkflowrecord(record2);
|
|
18351
|
+
await memory.addworkflowversion({ workflowid: record2.id, version: record2.version, createdat: Date.now(), note: typeof inputsave.note === "string" && inputsave.note.trim() !== "" ? inputsave.note.trim() : `Edited on the canvas with ${record2.steps.length} expanded steps`, steps: record2.steps.length, risk: record2.risk });
|
|
18352
|
+
await memory.seteditorlayout(record2.id, model.layout);
|
|
18353
|
+
await memory.setworkflowbreakpoints(record2.id, model.nodes.flatMap((node) => node.step?.breakpoint === true ? [node.step.id] : []));
|
|
18354
|
+
await audit("workflow", `The user saved the workflow ${record2.name} as version ${record2.version} from the canvas editor: ${record2.steps.length} expanded step${record2.steps.length === 1 ? "" : "s"} graded ${record2.risk}; the composition ran the full workflow grammar and every older version survives for the timeline.`, { ...session !== void 0 ? { sessionid: session.id } : {}, ...plan !== void 0 ? { planid: plan.id } : {} });
|
|
18355
|
+
return { workflowid: record2.id, version: record2.version, steps: record2.steps.length, risk: record2.risk };
|
|
18356
|
+
}
|
|
18357
|
+
case "seteditorlayout": {
|
|
18358
|
+
const inputlayout = message;
|
|
18359
|
+
const workflowid = inputlayout.workflowid?.trim() ?? "";
|
|
18360
|
+
const layout = inputlayout.layout;
|
|
18361
|
+
if (!workflowid || !layout || !Number.isFinite(layout.width) || !Number.isFinite(layout.height) || !Number.isFinite(layout.zoom) || layout.zoom <= 0) throw new Error("The layout save needs the workflow id and a positive canvas layout.");
|
|
18362
|
+
await memory.seteditorlayout(workflowid, layout);
|
|
18363
|
+
return { workflowid, saved: true };
|
|
18364
|
+
}
|
|
18365
|
+
case "setworkflowbreakpoints": {
|
|
18366
|
+
const inputbreakpoints = message;
|
|
18367
|
+
const workflowid = inputbreakpoints.workflowid?.trim() ?? "";
|
|
18368
|
+
if (!workflowid) throw new Error("The breakpoint save needs the workflow id.");
|
|
18369
|
+
const stepids = Array.isArray(inputbreakpoints.stepids) ? inputbreakpoints.stepids.filter((id) => typeof id === "string" && id.trim() !== "") : [];
|
|
18370
|
+
await memory.setworkflowbreakpoints(workflowid, stepids);
|
|
18371
|
+
await audit("workflow", `The user set ${stepids.length} breakpoint${stepids.length === 1 ? "" : "s"} on the workflow ${workflowid}; a debug run pauses before every marked step.`, {});
|
|
18372
|
+
return { workflowid, breakpoints: stepids.length };
|
|
18373
|
+
}
|
|
18374
|
+
case "steplibrarystore": {
|
|
18375
|
+
return { categories: palettecategories, palette: palettenodes, library: buildsteplibrary(reviewedkinds()) };
|
|
18376
|
+
}
|
|
18377
|
+
case "importworkflow": {
|
|
18378
|
+
const inputimport = message;
|
|
18379
|
+
const session = await memory.getsession();
|
|
18380
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Importing a workflow file needs an active browser session behind the consent gates.");
|
|
18381
|
+
if (typeof inputimport.contents !== "string" || inputimport.contents.trim() === "") throw new Error("The workflow import needs the file contents.");
|
|
18382
|
+
const format = inputimport.format === "yaml" ? "yaml" : inputimport.format === "json" ? "json" : inputimport.contents.trimStart().startsWith("{") ? "json" : "yaml";
|
|
18383
|
+
const loaded = importworkflow({ contents: inputimport.contents, format, now: Date.now(), kindallowed: (kind) => {
|
|
18384
|
+
try {
|
|
18385
|
+
actionrisk(kind);
|
|
18386
|
+
return true;
|
|
18387
|
+
} catch {
|
|
18388
|
+
return false;
|
|
18389
|
+
}
|
|
18390
|
+
}, riskof: (kind) => actionrisk(kind) });
|
|
18391
|
+
const importid = randomid();
|
|
18392
|
+
await memory.addworkflowrecord(loaded.record);
|
|
18393
|
+
await memory.addworkflowimport({ id: importid, record: loaded.record, importedat: Date.now(), ...typeof inputimport.filename === "string" && inputimport.filename.trim() !== "" ? { filename: inputimport.filename.trim() } : {} });
|
|
18394
|
+
for (const template of loaded.templates) await memory.addsteptemplate(template);
|
|
18395
|
+
await audit("workflow", `Imported the workflow ${loaded.record.name} version ${loaded.record.version} from a ${format} file with ${loaded.record.steps.length} expanded step${loaded.record.steps.length === 1 ? "" : "s"} and ${loaded.templates.length} packed template${loaded.templates.length === 1 ? "" : "s"}; the record stays unreviewed until the import review approves its step list.`, { sessionid: session.id });
|
|
18396
|
+
return { importid, workflowid: loaded.record.id, name: loaded.record.name, version: loaded.record.version, steps: loaded.record.steps.length, risk: loaded.record.risk, reviewstate: "pending", templates: loaded.templates.length };
|
|
18397
|
+
}
|
|
18398
|
+
case "approveimport": {
|
|
18399
|
+
const inputapproveimport = message;
|
|
18400
|
+
const session = await memory.getsession();
|
|
18401
|
+
const pending = (await memory.listworkflowimports()).find((entry) => entry.id === (inputapproveimport.importid ?? ""));
|
|
18402
|
+
if (!pending) throw new Error(`No pending workflow import matches ${inputapproveimport.importid ?? ""}.`);
|
|
18403
|
+
const approved = { ...pending.record, reviewstate: "approved" };
|
|
18404
|
+
await memory.addworkflowrecord(approved);
|
|
18405
|
+
await memory.removeworkflowimport(pending.id);
|
|
18406
|
+
await audit("workflow", `The user approved the import review of the workflow ${approved.name} version ${approved.version} with its ${approved.steps.length} expanded step${approved.steps.length === 1 ? "" : "s"} shown; the workflow may now run behind the same gates as every composed workflow.`, { ...session !== void 0 ? { sessionid: session.id } : {} });
|
|
18407
|
+
return { workflowid: approved.id, version: approved.version, reviewstate: "approved" };
|
|
18408
|
+
}
|
|
18409
|
+
case "rejectimport": {
|
|
18410
|
+
const inputreject = message;
|
|
18411
|
+
const session = await memory.getsession();
|
|
18412
|
+
const pending = (await memory.listworkflowimports()).find((entry) => entry.id === (inputreject.importid ?? ""));
|
|
18413
|
+
if (!pending) throw new Error(`No pending workflow import matches ${inputreject.importid ?? ""}.`);
|
|
18414
|
+
await memory.removeworkflowversion(pending.record.id, pending.record.version);
|
|
18415
|
+
await memory.removeworkflowimport(pending.id);
|
|
18416
|
+
await audit("workflow", `The user rejected the import review of the workflow ${pending.record.name} version ${pending.record.version}; the pending record left the library and nothing of the import runs.`, { ...session !== void 0 ? { sessionid: session.id } : {} });
|
|
18417
|
+
return { rejected: true, importid: pending.id };
|
|
18418
|
+
}
|
|
18419
|
+
case "exportworkflow": {
|
|
18420
|
+
const inputexport = message;
|
|
18421
|
+
const session = await memory.getsession();
|
|
18422
|
+
const record2 = await memory.getworkflowrecord(inputexport.workflowid?.trim() ?? "");
|
|
18423
|
+
if (!record2) throw new Error(`No composed workflow matches ${inputexport.workflowid ?? ""}.`);
|
|
18424
|
+
const format = inputexport.format === "yaml" ? "yaml" : "json";
|
|
18425
|
+
const exported = exportworkflow(record2, format, typeof inputexport.note === "string" ? inputexport.note : void 0, Date.now());
|
|
18426
|
+
const review = exportcontentreview(exported.file);
|
|
18427
|
+
if (!review.allowed) throw new Error(review.reason ?? "The export carries a secret field and secrets never leave the browser.");
|
|
18428
|
+
await audit("workflow", `Exported the workflow ${record2.name} version ${record2.version} as a ${format} file with ${record2.steps.length} expanded step${record2.steps.length === 1 ? "" : "s"}; the export content review verified that no secret field leaves the browser.`, { ...session !== void 0 ? { sessionid: session.id } : {} });
|
|
18429
|
+
return { format, contents: exported.contents, filename: `${record2.name.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}-v${record2.version}.${format}` };
|
|
18430
|
+
}
|
|
18431
|
+
case "shareworkflow": {
|
|
18432
|
+
const inputshare = message;
|
|
18433
|
+
const session = await memory.getsession();
|
|
18434
|
+
const record2 = await memory.getworkflowrecord(inputshare.workflowid?.trim() ?? "");
|
|
18435
|
+
if (!record2) throw new Error(`No composed workflow matches ${inputshare.workflowid ?? ""}.`);
|
|
18436
|
+
const format = inputshare.format === "yaml" ? "yaml" : "json";
|
|
18437
|
+
const templates = await memory.getsteptemplates();
|
|
18438
|
+
const shared = shareworkflow(record2, templates, format, typeof inputshare.note === "string" ? inputshare.note : void 0, Date.now());
|
|
18439
|
+
const review = exportcontentreview(shared.file);
|
|
18440
|
+
if (!review.allowed) throw new Error(review.reason ?? "The share bundle carries a secret field and secrets never leave the browser.");
|
|
18441
|
+
await audit("workflow", `Packed the workflow ${record2.name} version ${record2.version} with ${templates.length} shared step template${templates.length === 1 ? "" : "s"} into one ${format} share file; the export content review verified that no secret field leaves the browser.`, { ...session !== void 0 ? { sessionid: session.id } : {} });
|
|
18442
|
+
return { format, contents: shared.contents, filename: `${record2.name.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}-share-v${record2.version}.${format}` };
|
|
18443
|
+
}
|
|
18444
|
+
case "rollbackversion": {
|
|
18445
|
+
const inputrollback = message;
|
|
18446
|
+
const session = await memory.getsession();
|
|
18447
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("A version rollback needs an active browser session behind the consent gates.");
|
|
18448
|
+
const versions = await memory.getworkflowrecordversions();
|
|
18449
|
+
const target = versions.find((entry) => entry.id === (inputrollback.workflowid?.trim() ?? "") && entry.version === inputrollback.version);
|
|
18450
|
+
if (!target) throw new Error(`No stored workflow version matches ${inputrollback.workflowid ?? ""} version ${String(inputrollback.version ?? "")}.`);
|
|
18451
|
+
const latest = versions.filter((entry) => entry.id === target.id).reduce((max, entry) => Math.max(max, entry.version), 0);
|
|
18452
|
+
const rolledback = composeworkflow({ id: target.id, name: target.name, version: latest + 1, origins: [...target.origins], steps: target.steps.map((step) => ({ ...step })), blocks: target.blocks.map((block) => ({ ...block })), now: Date.now(), kindallowed: (kind) => {
|
|
18453
|
+
try {
|
|
18454
|
+
actionrisk(kind);
|
|
18455
|
+
return true;
|
|
18456
|
+
} catch {
|
|
18457
|
+
return false;
|
|
18458
|
+
}
|
|
18459
|
+
}, riskof: (kind) => actionrisk(kind) });
|
|
18460
|
+
const pendingrollback = { ...rolledback, reviewstate: "pending" };
|
|
18461
|
+
await memory.addworkflowrecord(pendingrollback);
|
|
18462
|
+
await memory.addworkflowversion({ workflowid: rolledback.id, version: rolledback.version, createdat: Date.now(), note: `Rolled back to version ${target.version} of ${target.steps.length} steps`, steps: rolledback.steps.length, risk: rolledback.risk, rollback: true });
|
|
18463
|
+
await audit("workflow", `The user rolled the workflow ${target.name} back to version ${target.version}; the restored steps stored as version ${rolledback.version} and the rollback review gates its first run like a fresh import.`, { sessionid: session.id });
|
|
18464
|
+
return { workflowid: rolledback.id, version: rolledback.version, restoredfrom: target.version, steps: rolledback.steps.length, reviewstate: "pending" };
|
|
18465
|
+
}
|
|
18466
|
+
case "diffversions": {
|
|
18467
|
+
const inputdiff = message;
|
|
18468
|
+
const versions = await memory.getworkflowrecordversions();
|
|
18469
|
+
const from = versions.find((entry) => entry.id === (inputdiff.workflowid?.trim() ?? "") && entry.version === inputdiff.from);
|
|
18470
|
+
const to = versions.find((entry) => entry.id === (inputdiff.workflowid?.trim() ?? "") && entry.version === inputdiff.to);
|
|
18471
|
+
if (!from || !to) throw new Error(`The version diff needs two stored versions of ${inputdiff.workflowid ?? ""}.`);
|
|
18472
|
+
const diff = diffversions(from, to, Date.now());
|
|
18473
|
+
await memory.addversiondiff(diff);
|
|
18474
|
+
return diff;
|
|
18475
|
+
}
|
|
18476
|
+
case "runhistory": {
|
|
18477
|
+
const query = runhistoryquery(message);
|
|
18478
|
+
return runhistoryreport({ entries: await memory.gethistory(query), query });
|
|
18479
|
+
}
|
|
18480
|
+
case "setrunhistoryretention": {
|
|
18481
|
+
const inputretention = message;
|
|
18482
|
+
const settings = await memory.getsettings();
|
|
18483
|
+
const retention = typeof inputretention.retention === "number" && Number.isInteger(inputretention.retention) && inputretention.retention >= 0 ? inputretention.retention : void 0;
|
|
18484
|
+
await memory.setsettings({ ...settings, ...retention !== void 0 ? { runhistoryretention: retention } : {} });
|
|
18485
|
+
await audit("configure", `The user set the run history retention to ${retention === void 0 ? "keep every entry" : `${retention} entr${retention === 1 ? "y" : "ies"}`}; no code ceiling applies.`);
|
|
18486
|
+
return { runhistoryretention: retention };
|
|
18487
|
+
}
|
|
18488
|
+
case "setbackgroundrun": {
|
|
18489
|
+
const inputbackground = message;
|
|
18490
|
+
const workflowid = inputbackground.workflowid?.trim() ?? "";
|
|
18491
|
+
if (!workflowid) throw new Error("The background run toggle needs the workflow id.");
|
|
18492
|
+
const flags = await memory.getbackgroundruns();
|
|
18493
|
+
const enabled = inputbackground.enabled === true;
|
|
18494
|
+
await memory.setbackgroundruns({ ...flags, [workflowid]: enabled });
|
|
18495
|
+
await audit("workflow", `The user ${enabled ? "enabled" : "disabled"} the background run toggle of the workflow ${workflowid}${enabled ? "; its runs keep executing in the service worker with the panel closed and every worker wake restores an interrupted run through the same gates" : ""}.`, {});
|
|
18496
|
+
return { workflowid, enabled };
|
|
18497
|
+
}
|
|
18498
|
+
case "setwatchdog": {
|
|
18499
|
+
const inputwatchdog = message;
|
|
18500
|
+
const config = inputwatchdog.config;
|
|
18501
|
+
if (!config) throw new Error("The watchdog save needs the configuration.");
|
|
18502
|
+
const check = watchdogconfigvalid(config);
|
|
18503
|
+
if (!check.allowed) throw new Error(check.reason ?? "The watchdog configuration failed its review.");
|
|
18504
|
+
const settings = await memory.getsettings();
|
|
18505
|
+
await memory.setsettings({ ...settings, watchdog: config });
|
|
18506
|
+
await audit("configure", `The user configured the workflow watchdog: stall threshold ${config.stallthreshold} ms, recovery ${config.action}${config.zombiewindow !== void 0 ? `, zombie window ${config.zombiewindow} ms` : ""}; both stay user values with no code ceiling.`, {});
|
|
18507
|
+
return { watchdog: config };
|
|
18508
|
+
}
|
|
18509
|
+
case "watchdogscan": {
|
|
18510
|
+
const events = await runwatchdog();
|
|
18511
|
+
return { events, config: (await memory.getsettings())?.watchdog };
|
|
18512
|
+
}
|
|
18513
|
+
case "setsiteoverride": {
|
|
18514
|
+
const inputoverride = message;
|
|
18515
|
+
const session = await memory.getsession();
|
|
18516
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("A per site override needs an active browser session behind the consent gates.");
|
|
18517
|
+
const workflowid = inputoverride.workflowid?.trim() ?? "";
|
|
18518
|
+
if (!workflowid) throw new Error("The per site override needs the workflow id.");
|
|
18519
|
+
const record2 = await memory.getworkflowrecord(workflowid);
|
|
18520
|
+
if (!record2) throw new Error(`No composed workflow matches ${workflowid}.`);
|
|
18521
|
+
const deltas = inputoverride.deltas !== void 0 && typeof inputoverride.deltas === "object" && !Array.isArray(inputoverride.deltas) ? inputoverride.deltas : {};
|
|
18522
|
+
const check = validatesiteoverride({ pattern: inputoverride.pattern ?? "", deltas });
|
|
18523
|
+
if (!check.allowed) throw new Error(check.reason ?? "The per site override failed its review.");
|
|
18524
|
+
const override = { id: typeof inputoverride.id === "string" && inputoverride.id.trim() !== "" ? inputoverride.id : randomid(), workflowid, pattern: inputoverride.pattern.trim(), deltas, createdat: Date.now() };
|
|
18525
|
+
await memory.addsiteoverride(override);
|
|
18526
|
+
await audit("workflow", `The user attached the per site override ${override.pattern} to the workflow ${record2.name}: ${Object.entries(deltas).map(([knob, delta]) => `${knob} ${delta}`).join(", ") || "no delta"}; the override adjusts only the reviewed knobs.`, { sessionid: session.id });
|
|
18527
|
+
return { override };
|
|
18528
|
+
}
|
|
18529
|
+
case "removesiteoverride": {
|
|
18530
|
+
const inputremove = message;
|
|
18531
|
+
const removed = (await memory.listsiteoverrides()).find((entry) => entry.id === (inputremove.id ?? ""));
|
|
18532
|
+
if (!removed) throw new Error(`No per site override matches ${inputremove.id ?? ""}.`);
|
|
18533
|
+
await memory.removesiteoverride(removed.id);
|
|
18534
|
+
await audit("workflow", `The user removed the per site override ${removed.pattern} of the workflow ${removed.workflowid}.`, {});
|
|
18535
|
+
return { removed: removed.id };
|
|
18536
|
+
}
|
|
17021
18537
|
default:
|
|
17022
18538
|
throw new Error("Unknown Devthink request.");
|
|
17023
18539
|
}
|
|
17024
18540
|
}
|
|
18541
|
+
async function runworkflowsegment(input) {
|
|
18542
|
+
const segmentids = new Set(input.record.steps.slice(input.startcursor, input.until).map((entry) => entry.id));
|
|
18543
|
+
const steps = input.record.steps.slice(input.startcursor, input.until).map((entry) => {
|
|
18544
|
+
const bindings = (entry.bindings ?? []).filter((binding) => segmentids.has(binding.stepid));
|
|
18545
|
+
return { ...entry, ...bindings.length > 0 ? { bindings } : {} };
|
|
18546
|
+
});
|
|
18547
|
+
const segmentrecord = composeworkflow({ id: input.record.id, name: input.record.name, version: input.record.version, origins: [...input.record.origins], steps, blocks: [], now: Date.now(), kindallowed: (kind) => {
|
|
18548
|
+
try {
|
|
18549
|
+
actionrisk(kind);
|
|
18550
|
+
return true;
|
|
18551
|
+
} catch {
|
|
18552
|
+
return false;
|
|
18553
|
+
}
|
|
18554
|
+
}, riskof: (kind) => actionrisk(kind) });
|
|
18555
|
+
const { pausedat, ...baserun } = input.run;
|
|
18556
|
+
void pausedat;
|
|
18557
|
+
const segmentrun = { ...baserun, state: "running", cursor: 0 };
|
|
18558
|
+
let storedentries = input.storedentries;
|
|
18559
|
+
const result = await runworkflow({ record: segmentrecord, run: segmentrun, scopes: input.scopes, log: input.log, execute: input.execute, now: Date.now(), gates: { sessionactive: Boolean(input.session && !input.session.stoppedat && !input.session.pausedat && input.session.expiresat > Date.now()), planapproved: input.plan.state === "approved", origingranted: (workfloworigin) => origingranted(input.session, workfloworigin) }, oncheckpoint: async (state) => {
|
|
18560
|
+
await memory.setworkflowrun({ ...state.run, cursor: input.startcursor + state.run.cursor, ...state.run.background === true ? { background: true } : {} });
|
|
18561
|
+
for (const entry of state.log.slice(storedentries)) await memory.addrunlogentry(state.run.id, entry);
|
|
18562
|
+
storedentries = state.log.length;
|
|
18563
|
+
await memory.setrunscopes(state.run.id, state.scopes);
|
|
18564
|
+
} });
|
|
18565
|
+
for (const entry of result.log.slice(storedentries)) await memory.addrunlogentry(input.run.id, entry);
|
|
18566
|
+
for (const entry of result.log.slice(input.log.length)) {
|
|
18567
|
+
const control = entry.details?.control;
|
|
18568
|
+
if (control && typeof control === "object" && !Array.isArray(control)) {
|
|
18569
|
+
const decision = control;
|
|
18570
|
+
await memory.addcontroldecision(input.run.id, decision);
|
|
18571
|
+
await auditcontroldecision(input.run.id, decision, input.session.id, input.plan.id);
|
|
18572
|
+
}
|
|
18573
|
+
}
|
|
18574
|
+
await memory.setrunscopes(input.run.id, result.scopes);
|
|
18575
|
+
const mapped = { ...input.run, state: result.run.state, cursor: input.startcursor + result.run.cursor, ...result.run.failreason !== void 0 ? { failreason: result.run.failreason } : {}, ...result.run.endedat !== void 0 ? { endedat: result.run.endedat } : {} };
|
|
18576
|
+
return { run: mapped, scopes: result.scopes, log: result.log, ...result.run.state === "failed" ? { failed: result.run.failreason ?? "The segment failed." } : {} };
|
|
18577
|
+
}
|
|
18578
|
+
async function runwatchdog() {
|
|
18579
|
+
const config = (await memory.getsettings())?.watchdog;
|
|
18580
|
+
if (!config || !config.enabled) return [];
|
|
18581
|
+
const runs = await memory.listworkflowruns();
|
|
18582
|
+
const lastcompletedat = {};
|
|
18583
|
+
for (const run of runs) {
|
|
18584
|
+
const log = await memory.getrunlog(run.id);
|
|
18585
|
+
const last = [...log].reverse().find((entry) => entry.state === "done");
|
|
18586
|
+
lastcompletedat[run.id] = last !== void 0 ? last.startedat + last.duration : run.startedat;
|
|
18587
|
+
}
|
|
18588
|
+
const verdicts = watchdogpass({ runs, lastcompletedat, liveexecutors: [...activeworkflowruns.keys()], config, now: Date.now() });
|
|
18589
|
+
const events = [];
|
|
18590
|
+
for (const verdict of verdicts) {
|
|
18591
|
+
if (verdict.verdict === "healthy") continue;
|
|
18592
|
+
const run = runs.find((entry) => entry.id === verdict.runid);
|
|
18593
|
+
if (!run) continue;
|
|
18594
|
+
let outcome = "";
|
|
18595
|
+
if (verdict.action === "pause") {
|
|
18596
|
+
const guards = activeworkflowruns.get(run.id);
|
|
18597
|
+
if (guards) guards.cancelled = true;
|
|
18598
|
+
await memory.setworkflowrun({ ...run, state: "paused", pausedat: Date.now(), pausekind: "watchdog" });
|
|
18599
|
+
outcome = `Paused at the checkpoint of step cursor ${run.cursor}; a user resume continues exactly there.`;
|
|
18600
|
+
} else if (verdict.action === "cancel" || verdict.action === "reap") {
|
|
18601
|
+
const guards = activeworkflowruns.get(run.id);
|
|
18602
|
+
if (guards) guards.cancelled = true;
|
|
18603
|
+
const cancelled = cancelrun(run, verdict.action === "reap" ? "The watchdog reaped the run as a zombie of a browser shutdown." : "The watchdog cancelled the stalled run.", Date.now());
|
|
18604
|
+
await memory.setworkflowrun(cancelled);
|
|
18605
|
+
outcome = verdict.action === "reap" ? "Reaped as a zombie of a browser shutdown at its last checkpoint." : "Cancelled as stalled beyond the reviewed threshold.";
|
|
18606
|
+
} else if (verdict.action === "retry") {
|
|
18607
|
+
await handlerequest({ kind: "resumeworkflowrun", runid: run.id }, {}).catch(() => {
|
|
18608
|
+
});
|
|
18609
|
+
outcome = `Retried the stalled step at cursor ${run.cursor} through the run gates.`;
|
|
18610
|
+
}
|
|
18611
|
+
const event = { id: randomid(), runid: run.id, verdict: verdict.verdict, action: verdict.action, outcome: `${verdict.reason} ${outcome}`.trim(), at: Date.now() };
|
|
18612
|
+
await memory.addwatchdogevent(event);
|
|
18613
|
+
await audit("workflow", `The watchdog marked the run ${run.id} ${verdict.verdict} and recovered it with ${verdict.action}: ${outcome}`, {});
|
|
18614
|
+
events.push(event);
|
|
18615
|
+
}
|
|
18616
|
+
await refreshbadge().catch(() => {
|
|
18617
|
+
});
|
|
18618
|
+
return events;
|
|
18619
|
+
}
|
|
18620
|
+
async function restorebackgroundruns() {
|
|
18621
|
+
const session = await memory.getsession();
|
|
18622
|
+
const plan = await memory.getplan();
|
|
18623
|
+
if (!session || session.stoppedat || session.pausedat || session.expiresat <= Date.now() || !plan || plan.state !== "approved") return;
|
|
18624
|
+
const flags = await memory.getbackgroundruns();
|
|
18625
|
+
for (const run of await memory.listworkflowruns()) {
|
|
18626
|
+
if (run.state !== "paused" || run.background !== true || run.pausekind === "user" || run.pausekind === "watchdog") continue;
|
|
18627
|
+
if (flags[run.workflowid] !== true) continue;
|
|
18628
|
+
const record2 = await memory.getworkflowrecord(run.workflowid);
|
|
18629
|
+
if (!record2) continue;
|
|
18630
|
+
let granted = true;
|
|
18631
|
+
for (const workfloworigin of record2.origins) {
|
|
18632
|
+
if (!origingranted(session, workfloworigin)) granted = false;
|
|
18633
|
+
}
|
|
18634
|
+
if (!granted) continue;
|
|
18635
|
+
const resumed = await handlerequest({ kind: "resumeworkflowrun", runid: run.id }, {}).catch(() => void 0);
|
|
18636
|
+
if (resumed === void 0) continue;
|
|
18637
|
+
await audit("workflow", `The worker wake restored the background run ${run.id} of the workflow ${record2.name} from its checkpoint at step cursor ${run.cursor}; the session, plan and origin gates re-passed.`, { sessionid: session.id, planid: plan.id });
|
|
18638
|
+
}
|
|
18639
|
+
}
|
|
18640
|
+
async function mcpconfigof() {
|
|
18641
|
+
return await memory.getmcpconfig() ?? defaultmcpconfig();
|
|
18642
|
+
}
|
|
18643
|
+
async function mcpstateof() {
|
|
18644
|
+
const config = await mcpconfigof();
|
|
18645
|
+
const state = await memory.getmcpstate();
|
|
18646
|
+
const binding = bindlocalhost(config);
|
|
18647
|
+
return { state: state?.state ?? "stopped", config, bind: binding.bind, port: binding.port, localhost: binding.localhost, clients: await memory.listclients(), ...state?.bridge !== void 0 ? { bridge: state.bridge } : {}, calls: (await memory.listtoolcalls()).slice(0, 25), catalog: listtools(buildtoolcatalog()), launches: (await memory.listbridgelaunches()).slice(0, 10) };
|
|
18648
|
+
}
|
|
18649
|
+
async function executelistruns(step, session) {
|
|
18650
|
+
const options = stepoptions2(step);
|
|
18651
|
+
const statefilter = typeof options.state === "string" && options.state.trim() !== "" ? options.state : void 0;
|
|
18652
|
+
const runs = await memory.listworkflowruns();
|
|
18653
|
+
const selected = statefilter !== void 0 ? runs.filter((run) => run.state === statefilter) : runs;
|
|
18654
|
+
await audit("workflow", `Listed ${selected.length} stored workflow run record${selected.length === 1 ? "" : "s"}${statefilter !== void 0 ? ` of the ${statefilter} state` : ""} from local memory as the read only listruns step; the listing carries no page data.`, { ...session ? { sessionid: session.id } : {}, stepid: step.id });
|
|
18655
|
+
return { ok: true, summary: `Listed ${selected.length} stored workflow run${selected.length === 1 ? "" : "s"} from local memory.`, details: { runs: selected.map((run) => ({ id: run.id, workflowid: run.workflowid, state: run.state, cursor: run.cursor, startedat: run.startedat, ...run.dryrun === true ? { dryrun: true } : {} })) } };
|
|
18656
|
+
}
|
|
18657
|
+
async function executemcpstep(step) {
|
|
18658
|
+
const session = await memory.getsession();
|
|
18659
|
+
if (!session) throw new Error("No active browser session exists.");
|
|
18660
|
+
const plan = await memory.getplan();
|
|
18661
|
+
const output = await executeaction(step, session, plan, session.tabid, session.origin, await memory.getsettings(), void 0, "plan");
|
|
18662
|
+
return { content: output.summary, ...output.details !== void 0 ? { payload: output.details } : {}, iserror: !output.ok };
|
|
18663
|
+
}
|
|
18664
|
+
async function trybridgelaunch(restart) {
|
|
18665
|
+
const host = "com.wenathlan.devthink";
|
|
18666
|
+
const runtime = chrome.runtime;
|
|
18667
|
+
try {
|
|
18668
|
+
const port = runtime.connectNative?.(host);
|
|
18669
|
+
if (!port) throw new Error("The browser exposes no native messaging api under the current permission set.");
|
|
18670
|
+
const previous = (await memory.getmcpstate())?.bridge;
|
|
18671
|
+
const bridge = restart && previous !== void 0 ? restartbridgeof(previous) : launchbridge({ host, now: Date.now() });
|
|
18672
|
+
port.onDisconnect.addListener(() => {
|
|
18673
|
+
void (async () => {
|
|
18674
|
+
const state = await memory.getmcpstate();
|
|
18675
|
+
if (state?.bridge !== void 0) await memory.setmcpstate({ ...state, bridge: { ...state.bridge, connected: false } });
|
|
18676
|
+
})().catch(() => {
|
|
18677
|
+
});
|
|
18678
|
+
});
|
|
18679
|
+
port.onMessage.addListener((message) => {
|
|
18680
|
+
if (typeof message !== "string") return;
|
|
18681
|
+
void (async () => {
|
|
18682
|
+
port.postMessage(serializeframe(await relaystdin(message)));
|
|
18683
|
+
})().catch(() => {
|
|
18684
|
+
});
|
|
18685
|
+
});
|
|
18686
|
+
return bridge;
|
|
18687
|
+
} catch {
|
|
18688
|
+
return void 0;
|
|
18689
|
+
}
|
|
18690
|
+
}
|
|
18691
|
+
function restartbridgeof(bridge) {
|
|
18692
|
+
return { ...bridge, connected: true, restarts: bridge.restarts + 1, startedat: Date.now() };
|
|
18693
|
+
}
|
|
18694
|
+
async function routemcpframe(client, frame, config) {
|
|
18695
|
+
const session = await memory.getsession();
|
|
18696
|
+
const plan = await memory.getplan();
|
|
18697
|
+
const catalog = buildtoolcatalog();
|
|
18698
|
+
const response = await handleframe({ frame, client, catalog, config, ...session !== void 0 ? { session } : {}, ...plan !== void 0 ? { plan } : {}, origin: session?.origin ?? "", tabid: session?.tabid ?? 0, now: Date.now(), execute: executemcpstep });
|
|
18699
|
+
const now = Date.now();
|
|
18700
|
+
if (frame.method === "initialize") {
|
|
18701
|
+
const clientinfo = frame.params?.clientinfo && typeof frame.params.clientinfo === "object" && !Array.isArray(frame.params.clientinfo) ? frame.params.clientinfo : void 0;
|
|
18702
|
+
const pid = typeof clientinfo?.pid === "number" && Number.isFinite(clientinfo.pid) ? clientinfo.pid : void 0;
|
|
18703
|
+
if (pid !== void 0) {
|
|
18704
|
+
const bridge = (await memory.getmcpstate())?.bridge;
|
|
18705
|
+
await memory.addbridgelaunch({ id: randomid(), host: bridge?.host ?? "com.wenathlan.devthink", pid, restart: false, at: now });
|
|
18706
|
+
}
|
|
18707
|
+
await audit("protocol", `The mcp client ${client.id} completed the initialize handshake${pid !== void 0 ? ` and reported its process id ${pid} for the bridge launch record` : ""}; the server offered protocol version ${String(response.result?.protocolversion ?? "") || "its own"} and the client waits for the pairing approval.`, {});
|
|
18708
|
+
}
|
|
18709
|
+
if (frame.method === "tools/list") await audit("protocol", `The mcp client ${client.id} listed the tool catalog of ${listtools(catalog).tools.length} tools with their json schema inputs; the listing carries no page data.`, {});
|
|
18710
|
+
if (frame.method === "negotiate") {
|
|
18711
|
+
const agreed = response.error === void 0;
|
|
18712
|
+
if (agreed && response.result !== void 0) await memory.setclientcapabilities(client.id, response.result);
|
|
18713
|
+
await audit("protocol", `The mcp client ${client.id} ${agreed ? "negotiated its capability set with the server and the negotiated floor stays stored on its record" : `failed the capability negotiation: ${response.error?.message ?? "the sets did not agree"}`}.`, {});
|
|
18714
|
+
}
|
|
18715
|
+
if (frame.method === "tools/call") {
|
|
18716
|
+
const name = typeof frame.params?.name === "string" ? frame.params.name : "";
|
|
18717
|
+
const code = response.error?.code;
|
|
18718
|
+
const ok = response.error === void 0 && response.result?.iserror !== true;
|
|
18719
|
+
await memory.addtoolcall(toolcallevent({ id: randomid(), clientid: client.id, tool: name, origin: session?.origin ?? "", ok, now, ...code !== void 0 ? { code } : {} }));
|
|
18720
|
+
if (plan !== void 0) {
|
|
18721
|
+
const stepid = typeof frame.params?.stepid === "string" ? frame.params.stepid : "mcp";
|
|
18722
|
+
await memory.setprogress(recordtoolcall(await memory.getprogress(), plan.id, stepid, { clientid: client.id, tool: name, ok, ...code !== void 0 ? { code } : {} }, now));
|
|
18723
|
+
}
|
|
18724
|
+
await audit("tool", `The mcp client ${client.id} called the ${name} tool on ${session?.origin ?? "no origin"} and ${ok ? "it ran behind the consent gates" : `it was refused${code !== void 0 ? ` with the ${code} error` : ""}`}; no payload rides the record.`, {});
|
|
18725
|
+
}
|
|
18726
|
+
return response;
|
|
18727
|
+
}
|
|
18728
|
+
async function processmcpframe(raw, clientid, transport) {
|
|
18729
|
+
const state = await memory.getmcpstate();
|
|
18730
|
+
if (state?.state !== "running") return { jsonrpc: "2.0", id: null, error: rpcerrorof("consentrefused", "The mcp server is not running and no frame is routed.") };
|
|
18731
|
+
const config = await mcpconfigof();
|
|
18732
|
+
if (!config.transports.includes(transport)) return { jsonrpc: "2.0", id: null, error: rpcerrorof("consentrefused", `The ${transport} transport is not allowed by the server configuration.`) };
|
|
18733
|
+
let frame;
|
|
18734
|
+
try {
|
|
18735
|
+
frame = parseframe(raw);
|
|
18736
|
+
} catch {
|
|
18737
|
+
await audit("protocol", "The mcp server refused a wire frame that does not parse as json; the parse error answered the client.", {});
|
|
18738
|
+
return { jsonrpc: "2.0", id: null, error: rpcerrorof("parse", "The wire frame does not parse as json.") };
|
|
18739
|
+
}
|
|
18740
|
+
let client = (await memory.getclients()).find((entry) => entry.id === clientid && entry.disconnectedat === void 0);
|
|
18741
|
+
if (client === void 0) {
|
|
18742
|
+
client = connectclient({ transport, now: Date.now(), ...clientid !== "" ? { id: clientid } : {} });
|
|
18743
|
+
await memory.setclient(client);
|
|
18744
|
+
await audit("protocol", `A new mcp client ${client.id} connected on the ${transport} transport and waits for the pairing approval; unpaired clients never dispatch tools.`, {});
|
|
18745
|
+
}
|
|
18746
|
+
const previous = mcpclientchains.get(client.id) ?? Promise.resolve();
|
|
18747
|
+
const task = previous.catch(() => void 0).then(async () => await routemcpframe(client, frame, config));
|
|
18748
|
+
mcpclientchains.set(client.id, task);
|
|
18749
|
+
return task;
|
|
18750
|
+
}
|
|
18751
|
+
async function relaystdin(raw) {
|
|
18752
|
+
const now = Date.now();
|
|
18753
|
+
let state = await memory.getmcpstate();
|
|
18754
|
+
if (state?.bridge !== void 0) {
|
|
18755
|
+
let frame;
|
|
18756
|
+
try {
|
|
18757
|
+
frame = parseframe(raw);
|
|
18758
|
+
} catch {
|
|
18759
|
+
frame = void 0;
|
|
18760
|
+
}
|
|
18761
|
+
if (frame !== void 0) {
|
|
18762
|
+
await memory.setmcpstate({ ...state, bridge: relayframe({ bridge: state.bridge, direction: "inbound", frame, now }) });
|
|
18763
|
+
state = await memory.getmcpstate();
|
|
18764
|
+
}
|
|
18765
|
+
}
|
|
18766
|
+
const response = await processmcpframe(raw, "", "stdio");
|
|
18767
|
+
if (state?.bridge !== void 0) await memory.setmcpstate({ ...state, bridge: relayframe({ bridge: state.bridge, direction: "outbound", frame: response, now: Date.now() }) });
|
|
18768
|
+
return response;
|
|
18769
|
+
}
|
|
18770
|
+
var mcpclientchains = /* @__PURE__ */ new Map();
|
|
17025
18771
|
chrome.runtime.onMessage.addListener((message, sender, sendresponse) => {
|
|
17026
18772
|
handlerequest(message, sender).then((value) => sendresponse({ ok: true, value })).catch((error) => sendresponse({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
17027
18773
|
return true;
|
|
@@ -17042,12 +18788,15 @@ async function detectcrash() {
|
|
|
17042
18788
|
}
|
|
17043
18789
|
chrome.runtime.onStartup.addListener(() => {
|
|
17044
18790
|
void detectcrash();
|
|
17045
|
-
void pauseinterruptedworkflowruns()
|
|
18791
|
+
void pauseinterruptedworkflowruns().then(() => restorebackgroundruns()).catch(() => {
|
|
18792
|
+
});
|
|
18793
|
+
void runwatchdog().catch(() => {
|
|
18794
|
+
});
|
|
17046
18795
|
});
|
|
17047
18796
|
async function pauseinterruptedworkflowruns() {
|
|
17048
18797
|
for (const run of await memory.listworkflowruns()) {
|
|
17049
18798
|
if (run.state !== "running") continue;
|
|
17050
|
-
await memory.setworkflowrun({ ...run, state: "paused", pausedat: Date.now() });
|
|
18799
|
+
await memory.setworkflowrun({ ...run, state: "paused", pausedat: Date.now(), pausekind: "interrupt" });
|
|
17051
18800
|
await audit("workflow", `The service worker restart paused the workflow run ${run.id} at its last checkpoint of step cursor ${run.cursor}; the resume continues exactly there.`, {});
|
|
17052
18801
|
}
|
|
17053
18802
|
}
|
|
@@ -17181,13 +18930,17 @@ chrome.runtime.onConnect.addListener((port) => {
|
|
|
17181
18930
|
}
|
|
17182
18931
|
}
|
|
17183
18932
|
setInterval(() => {
|
|
17184
|
-
void evaluatelistedtriggers().then(() => draintriggerqueue()).catch(() => {
|
|
18933
|
+
void evaluatelistedtriggers().then(() => draintriggerqueue()).then(() => runwatchdog()).then(() => restorebackgroundruns()).catch(() => {
|
|
17185
18934
|
});
|
|
17186
18935
|
}, 3e4);
|
|
17187
18936
|
async function restoretriggers() {
|
|
17188
18937
|
await registermenurules();
|
|
17189
18938
|
await evaluatelistedtriggers();
|
|
17190
18939
|
await draintriggerqueue();
|
|
18940
|
+
await runwatchdog().catch(() => {
|
|
18941
|
+
});
|
|
18942
|
+
await restorebackgroundruns().catch(() => {
|
|
18943
|
+
});
|
|
17191
18944
|
}
|
|
17192
18945
|
restoretriggers().catch(() => {
|
|
17193
18946
|
});
|