@skyramp/mcp 0.5.0-rc.1 → 0.5.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/build/entitlements/entitlementGate.d.ts +8 -0
  2. package/build/entitlements/entitlementGate.js +57 -0
  3. package/build/entitlements/runCodes.d.ts +18 -0
  4. package/build/entitlements/runCodes.js +16 -0
  5. package/build/index.js +31 -2
  6. package/build/playwright/PlaywrightTraceService.d.ts +17 -0
  7. package/build/playwright/PlaywrightTraceService.js +29 -1
  8. package/build/playwright/dataSafetyConfig.d.ts +74 -0
  9. package/build/playwright/dataSafetyConfig.js +109 -0
  10. package/build/playwright/essentialTools.d.ts +1 -0
  11. package/build/playwright/essentialTools.js +44 -0
  12. package/build/playwright/registerPlaywrightTools.d.ts +36 -1
  13. package/build/playwright/registerPlaywrightTools.js +87 -43
  14. package/build/playwright/safeResolveDataSafety.d.ts +24 -0
  15. package/build/playwright/safeResolveDataSafety.js +33 -0
  16. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +1 -1
  17. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +22 -196
  18. package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
  19. package/build/prompts/modularization/ui-test-modularization.js +9 -2
  20. package/build/services/FleetExecutionService.d.ts +87 -16
  21. package/build/services/FleetExecutionService.js +487 -48
  22. package/build/services/FleetRunRegistry.d.ts +49 -0
  23. package/build/services/FleetRunRegistry.js +178 -0
  24. package/build/services/containerEnv.js +8 -2
  25. package/build/services/fleetLimitConfirmation.d.ts +19 -0
  26. package/build/services/fleetLimitConfirmation.js +126 -0
  27. package/build/tool-phases.d.ts +14 -8
  28. package/build/tool-phases.js +88 -43
  29. package/build/tools/code-refactor/assertion-state.js +3 -1
  30. package/build/tools/code-refactor/codeReuseTool.js +2 -1
  31. package/build/tools/code-refactor/utils-verify-gates.js +1 -1
  32. package/build/tools/executeFleetTool.d.ts +6 -4
  33. package/build/tools/executeFleetTool.js +67 -23
  34. package/build/tools/fixErrorTool.js +2 -1
  35. package/build/tools/fleetLifecycleTools.d.ts +20 -0
  36. package/build/tools/fleetLifecycleTools.js +124 -0
  37. package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +38 -0
  38. package/build/tools/generate-tests/generateIntegrationRestTool.js +7 -1
  39. package/build/tools/generate-tests/generateUILoadRestTool.d.ts +19 -1
  40. package/build/tools/generate-tests/generateUILoadRestTool.js +316 -50
  41. package/build/tools/test-management/registerTestPlanTool.js +22 -6
  42. package/build/tools/workspace/initScanWorkspaceTool.js +16 -5
  43. package/build/types/AssertionOutcome.d.ts +3 -0
  44. package/build/utils/assertion-verify/created-add-guard.d.ts +12 -0
  45. package/build/utils/assertion-verify/created-add-guard.js +173 -0
  46. package/build/utils/assertion-verify/index.d.ts +1 -0
  47. package/build/utils/assertion-verify/index.js +1 -0
  48. package/build/utils/assertion-verify/verify.d.ts +7 -3
  49. package/build/utils/assertion-verify/verify.js +9 -3
  50. package/build/utils/featureFlags.js +6 -3
  51. package/build/utils/repositorySlug.d.ts +14 -0
  52. package/build/utils/repositorySlug.js +46 -1
  53. package/build/utils/telemetry.d.ts +2 -0
  54. package/build/utils/telemetry.js +12 -0
  55. package/build/utils/utils-verify/action-key.d.ts +59 -2
  56. package/build/utils/utils-verify/action-key.js +162 -6
  57. package/build/utils/utils-verify/action-sites.d.ts +7 -0
  58. package/build/utils/utils-verify/action-sites.js +277 -35
  59. package/build/utils/utils-verify/call-sites.d.ts +8 -2
  60. package/build/utils/utils-verify/language-spec.js +1 -1
  61. package/build/utils/utils-verify/parse.d.ts +4 -0
  62. package/build/utils/utils-verify/parse.js +2 -1
  63. package/build/utils/versions.d.ts +3 -3
  64. package/build/utils/versions.js +1 -1
  65. package/build/workspace/workspace.d.ts +54 -120
  66. package/build/workspace/workspace.js +22 -46
  67. package/build/workspace/workspaceVerifier.d.ts +4 -10
  68. package/build/workspace/workspaceVerifier.js +6 -248
  69. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +7 -6
  70. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +5 -4
  71. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +12 -0
  72. package/node_modules/playwright/lib/mcp/skyramp/dataSafetyGuard.js +822 -0
  73. package/node_modules/playwright/lib/mcp/skyramp/dataSafetySession.js +372 -0
  74. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +101 -8
  75. package/node_modules/playwright/lib/mcp/skyramp/finishTestSetupTool.js +45 -0
  76. package/node_modules/playwright/lib/mcp/skyramp/index.js +16 -0
  77. package/node_modules/playwright/lib/mcp/skyramp/recordingPhase.js +65 -0
  78. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +1 -0
  79. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +598 -127
  80. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +85 -31
  81. package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +132 -5
  82. package/node_modules/playwright/node_modules/playwright-core/lib/cli/program.js +2 -0
  83. package/node_modules/playwright/node_modules/playwright-core/lib/client/browserContext.js +4 -2
  84. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  85. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  86. package/node_modules/playwright/node_modules/playwright-core/lib/protocol/validator.js +2 -1
  87. package/node_modules/playwright/node_modules/playwright-core/lib/server/browserContext.js +17 -1
  88. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  89. package/node_modules/playwright/node_modules/playwright-core/lib/server/har/harRecorder.js +1 -0
  90. package/node_modules/playwright/node_modules/playwright-core/lib/server/har/harTracer.js +1 -1
  91. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderRunner.js +2 -1
  92. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -1
  93. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/clickExpandedState.js +75 -0
  94. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +4 -2
  95. package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +85 -0
  96. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Bou8kKzE.js → codeMirrorModule-BuhAdckO.js} +1 -1
  97. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-COAHN430.js +809 -0
  98. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.0MJW0uG7.js → index.7AU8h0PZ.js} +1 -1
  99. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  100. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.GLqsx5cI.js → uiMode.Dl7smBJn.js} +1 -1
  101. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  102. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  103. package/node_modules/playwright/node_modules/playwright-core/src/cli/program.ts +5 -0
  104. package/node_modules/playwright/node_modules/playwright-core/src/client/browserContext.ts +3 -1
  105. package/node_modules/playwright/node_modules/playwright-core/src/client/types.ts +2 -0
  106. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  107. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  108. package/node_modules/playwright/node_modules/playwright-core/src/protocol/validator.ts +1 -0
  109. package/node_modules/playwright/node_modules/playwright-core/src/server/browserContext.ts +25 -1
  110. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +2 -0
  111. package/node_modules/playwright/node_modules/playwright-core/src/server/har/harRecorder.ts +1 -0
  112. package/node_modules/playwright/node_modules/playwright-core/src/server/har/harTracer.ts +3 -1
  113. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderRunner.ts +3 -1
  114. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +1 -0
  115. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/clickExpandedState.ts +82 -0
  116. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +2 -0
  117. package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +137 -1
  118. package/node_modules/playwright/package.json +1 -1
  119. package/package.json +4 -4
  120. package/plugin/skills/enhance-assertions/reference/integration.md +1 -0
  121. package/plugin/skills/setup-testbot-target-sut/SKILL.md +17 -11
  122. package/plugin/skills/setup-testbot-target-sut/reference/deployed-environment.md +105 -0
  123. package/plugin/skills/setup-testbot-target-sut/reference/existing-tests.md +1 -1
  124. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-DTp2-8Si.js +0 -809
@@ -0,0 +1,8 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
+ import { type RunCodes } from "./runCodes.js";
3
+ /**
4
+ * Makes every tool registered on `server` after this call check the run's
5
+ * entitlement codes before it runs. Without a run token it does nothing, so
6
+ * every tool behaves as before. Must be called before any tool is registered.
7
+ */
8
+ export declare function installEntitlementGate(server: McpServer, readCodes?: () => Promise<RunCodes>, token?: string | undefined): void;
@@ -0,0 +1,57 @@
1
+ import { readRunCodes } from "./runCodes.js";
2
+ import { NOT_GATED, TOOLS } from "../tool-phases.js";
3
+ const FEATURE_NAMES = {
4
+ analyze: "change analysis",
5
+ recommend: "test recommendations",
6
+ generate: "test generation",
7
+ execute: "test execution",
8
+ };
9
+ function refusal(text) {
10
+ return { content: [{ type: "text", text }], isError: true };
11
+ }
12
+ /** Returns why `tool` may not run with these codes, or undefined if it may. */
13
+ function refusalReason(tool, code, run) {
14
+ if ("refused" in run) {
15
+ return `Tool ${tool} is not available: Skyramp could not confirm what this run is allowed to do. ${run.refused}`;
16
+ }
17
+ if (run.codes.has(code))
18
+ return undefined;
19
+ return `Tool ${tool} is not available: your Skyramp plan does not include ${FEATURE_NAMES[code]}.`;
20
+ }
21
+ /**
22
+ * Makes every tool registered on `server` after this call check the run's
23
+ * entitlement codes before it runs. Without a run token it does nothing, so
24
+ * every tool behaves as before. Must be called before any tool is registered.
25
+ */
26
+ export function installEntitlementGate(server, readCodes = readRunCodes, token = process.env.SKYRAMP_RUN_TOKEN) {
27
+ if (!token?.trim())
28
+ return;
29
+ let codes;
30
+ const runCodes = () => (codes ??= readCodes()
31
+ .catch((err) => ({
32
+ refused: err instanceof Error ? err.message : String(err),
33
+ }))
34
+ .then((run) => {
35
+ // A refusal can be temporary, so the next tool call reads again.
36
+ if ("refused" in run)
37
+ codes = undefined;
38
+ return run;
39
+ }));
40
+ // Typed loosely: the gate forwards every argument as it came, whatever
41
+ // overload the tool used.
42
+ const target = server;
43
+ const register = target.registerTool.bind(target);
44
+ target.registerTool = (name, config, cb) => {
45
+ const code = TOOLS[name]?.entitlement;
46
+ if (code === NOT_GATED)
47
+ return register(name, config, cb);
48
+ const handler = cb;
49
+ return register(name, config, async (...args) => {
50
+ if (code === undefined) {
51
+ return refusal(`Tool ${name} is not available in this run.`);
52
+ }
53
+ const reason = refusalReason(name, code, await runCodes());
54
+ return reason ? refusal(reason) : handler(...args);
55
+ });
56
+ };
57
+ }
@@ -0,0 +1,18 @@
1
+ export type RunCodes = {
2
+ codes: ReadonlySet<string>;
3
+ }
4
+ /** Core refused the run token; `refused` is its reason. */
5
+ | {
6
+ refused: string;
7
+ };
8
+ type Entitlements = {
9
+ codes: string[];
10
+ } | {
11
+ error: string;
12
+ };
13
+ /** The part of SkyrampClient this file uses. Core releases before getEntitlements lack it. */
14
+ export interface Core {
15
+ getEntitlements?: (repository: string) => Promise<Entitlements>;
16
+ }
17
+ export declare function readRunCodes(core?: Core, env?: NodeJS.ProcessEnv): Promise<RunCodes>;
18
+ export {};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The only file that may ask Skyramp core for the run's entitlement codes.
3
+ * Everything else in MCP reads them through `readRunCodes`, so a change on the
4
+ * core side changes this file and nothing else.
5
+ */
6
+ import { SkyrampClient } from "@skyramp/skyramp";
7
+ import { runRepository } from "../utils/repositorySlug.js";
8
+ export async function readRunCodes(core = new SkyrampClient(), env = process.env) {
9
+ if (!core.getEntitlements) {
10
+ return { refused: "This Skyramp version does not support it yet." };
11
+ }
12
+ const answer = await core.getEntitlements(runRepository(env) ?? "");
13
+ return "error" in answer
14
+ ? { refused: answer.error }
15
+ : { codes: new Set(answer.codes) };
16
+ }
package/build/index.js CHANGED
@@ -7,9 +7,12 @@ import { registerTraceTool } from "./tools/trace/startTraceCollectionTool.js";
7
7
  import { registerTraceStopTool } from "./tools/trace/stopTraceCollectionTool.js";
8
8
  import { registerExecuteSkyrampTestTool } from "./tools/executeSkyrampTestTool.js";
9
9
  import { registerExecuteFleetTool } from "./tools/executeFleetTool.js";
10
+ import { registerFleetLifecycleTools } from "./tools/fleetLifecycleTools.js";
11
+ import { fleetRuns } from "./services/FleetRunRegistry.js";
10
12
  import { AUTH_PLACEHOLDER_TOKEN } from "./types/TestTypes.js";
11
13
  import { AUTH_CONFLICT_ERROR_MSG } from "./prompts/test-recommendation/recommendationSections.js";
12
14
  import { logger } from "./utils/logger.js";
15
+ import { installEntitlementGate } from "./entitlements/entitlementGate.js";
13
16
  import { registerUITestTool } from "./tools/generate-tests/generateUIRestTool.js";
14
17
  import { registerUILoadTestTool } from "./tools/generate-tests/generateUILoadRestTool.js";
15
18
  import { registerSmokeTestTool } from "./tools/generate-tests/generateSmokeRestTool.js";
@@ -52,6 +55,7 @@ import { isTestbotEnabled, isLocalDevEnabled, isSkillsLoaded, } from "./utils/fe
52
55
  import { findRoot, readTestIdAttribute } from "./utils/pom-scope/index.js";
53
56
  import { discoverTestIdAttribute } from "./utils/pom-scope/testIdDiscovery.js";
54
57
  import { registerPlaywrightTools, registerTraceRecordingPrompt, getPlaywrightTraceService, } from "./playwright/index.js";
58
+ import { resolveDataSafetyOptions, playwrightToolOptionsFor, } from "./playwright/dataSafetyConfig.js";
55
59
  // One-click routing instructions — injected into MCP server instructions so agents
56
60
  // know to delegate to the one-click tool rather than self-orchestrating.
57
61
  // Suppressed in testbot mode where the testbot prompt drives orchestration.
@@ -155,6 +159,8 @@ async function createServer({ era }) {
155
159
  8. The user can always override workspace defaults by explicitly specifying values in their request.
156
160
  `,
157
161
  });
162
+ // Before any tool registers, so every tool goes through the check.
163
+ installEntitlementGate(server);
158
164
  // Register prompts
159
165
  logger.info("Starting prompt registration process");
160
166
  const prompts = [
@@ -239,6 +245,7 @@ async function createServer({ era }) {
239
245
  registerLogoutTool,
240
246
  registerExecuteSkyrampTestTool,
241
247
  registerExecuteFleetTool,
248
+ registerFleetLifecycleTools,
242
249
  registerTraceTool,
243
250
  registerTraceStopTool,
244
251
  ];
@@ -253,6 +260,11 @@ async function createServer({ era }) {
253
260
  infrastructureTools.forEach((registerTool) => registerTool(server));
254
261
  // Register Playwright browser tools (trace recording via browser automation).
255
262
  //
263
+ // PlaywrightTraceService (and the fork's network-layer data-safety guard it
264
+ // wraps) is constructed here, before any per-call `repositoryPath` exists,
265
+ // and `browser_navigate` itself takes no `repositoryPath` param, so
266
+ // process.cwd() is the only path data-safety config resolves from.
267
+ //
256
268
  // Tell the capture which attribute this repository uses for test ids. Without
257
269
  // it the capture assumes Playwright's `data-testid`, and a repo using anything
258
270
  // else gets no test-id anchor and a positional xpath for every element — which
@@ -309,7 +321,13 @@ async function createServer({ era }) {
309
321
  error: err,
310
322
  });
311
323
  }
312
- await registerPlaywrightTools(server, { testIdAttribute });
324
+ // SKYR-4047: thread data-safety options; resolveDataSafetyOptions returns
325
+ // undefined in the testbot lane (SKYRAMP_DATA_SAFETY env) so the fork reads env.
326
+ const dataSafety = await resolveDataSafetyOptions(process.cwd());
327
+ await registerPlaywrightTools(server, {
328
+ ...playwrightToolOptionsFor(dataSafety),
329
+ testIdAttribute,
330
+ });
313
331
  })().catch((err) => {
314
332
  logger.error("Failed to register Playwright tools", { error: err });
315
333
  });
@@ -325,11 +343,18 @@ async function createServer({ era }) {
325
343
  runInitTrigger();
326
344
  return server;
327
345
  }
346
+ // A crashing process must not stay alive anywhere near the full graceful
347
+ // shutdown's 30s grace — it still gives workers a moment to cancel, but caps
348
+ // it low enough that a crash can't hang the process.
349
+ const CRASH_CLEANUP_WAIT_MS = 3_000;
350
+ /** Interrupt running fleets (bounded wait) so an exit does not leave workers loading. */
351
+ const cancelFleets = () => fleetRuns.cancelAll(CRASH_CLEANUP_WAIT_MS).catch(() => { });
328
352
  // Global error handlers for crash telemetry
329
353
  process.on("uncaughtException", async (error) => {
330
354
  // Don't send analytics for EPIPE errors
331
355
  const isEpipeError = "code" in error && error.code === "EPIPE";
332
356
  if (isEpipeError) {
357
+ await cancelFleets();
333
358
  process.exit(0);
334
359
  }
335
360
  logger.critical("Uncaught exception - MCP server crashing", {
@@ -342,6 +367,7 @@ process.on("uncaughtException", async (error) => {
342
367
  catch (telemetryError) {
343
368
  logger.error("Failed to send crash telemetry", { telemetryError });
344
369
  }
370
+ await cancelFleets();
345
371
  process.exit(1);
346
372
  });
347
373
  // Handle unhandled promise rejections — without this handler Node.js 15+
@@ -351,6 +377,7 @@ process.on("unhandledRejection", async (reason) => {
351
377
  const error = reason instanceof Error ? reason : new Error(String(reason));
352
378
  // EPIPE means the client already disconnected — exit cleanly
353
379
  if ("code" in error && error.code === "EPIPE") {
380
+ await cancelFleets();
354
381
  process.exit(0);
355
382
  }
356
383
  // gRPC/Docker connectivity errors — log and survive.
@@ -397,7 +424,9 @@ async function main() {
397
424
  const cleanupPlaywright = playwrightService
398
425
  ? playwrightService.shutdown().catch(() => { })
399
426
  : Promise.resolve();
400
- cleanupPlaywright.finally(() => {
427
+ // A running fleet is interrupted so it can cancel its workers, not left loading them.
428
+ const cleanupFleets = fleetRuns.cancelAll().catch(() => { });
429
+ Promise.all([cleanupPlaywright, cleanupFleets]).finally(() => {
401
430
  handle.close().finally(() => {
402
431
  process.exit(0);
403
432
  });
@@ -6,6 +6,7 @@
6
6
  * to bridge the module systems.
7
7
  */
8
8
  import type { CallToolResult } from "@modelcontextprotocol/server";
9
+ import type { DataSafetyOptions } from "./dataSafetyConfig.js";
9
10
  interface McpTool {
10
11
  name: string;
11
12
  description?: string;
@@ -25,6 +26,8 @@ export interface PlaywrightTraceOptions {
25
26
  * against, so only this server can supply it.
26
27
  */
27
28
  testIdAttribute?: string;
29
+ /** Data safety: the workspace section, for the fork to validate. */
30
+ dataSafety?: DataSafetyOptions;
28
31
  }
29
32
  export declare class PlaywrightTraceService {
30
33
  private _backend;
@@ -35,6 +38,20 @@ export declare class PlaywrightTraceService {
35
38
  isInitialized(): boolean;
36
39
  getTools(): McpTool[];
37
40
  callTool(name: string, args: Record<string, unknown>): Promise<CallToolResult>;
41
+ /**
42
+ * How long shutdown() waits for the fork's session-end teardown, so a hung
43
+ * target cannot hold the process forever. Teardown runs after a 5 s
44
+ * body-read settle, in up to 3 DELETE passes; each DELETE times out after
45
+ * 10 s, and a pass in which every DELETE timed out is not retried. Several
46
+ * hung DELETEs can still use up this bound. The fork writes the ledger after
47
+ * each deletion, so the ledger file is still correct when the bound runs out.
48
+ */
49
+ static shutdownTeardownTimeoutMs: number;
50
+ /**
51
+ * The fork's serverClosed() starts data-safety teardown and returns at
52
+ * once. The caller exits the process right after this resolves, so wait
53
+ * for that work (bounded) — or the teardown DELETEs never reach the target.
54
+ */
38
55
  shutdown(): Promise<void>;
39
56
  }
40
57
  export {};
@@ -27,6 +27,7 @@ export class PlaywrightTraceService {
27
27
  headless: this._options.headless,
28
28
  outputDir: this._options.outputDir || process.cwd(),
29
29
  testIdAttribute: this._options.testIdAttribute,
30
+ dataSafety: this._options.dataSafety,
30
31
  });
31
32
  // Initialize with a synthetic client info
32
33
  await this._backend.initialize({
@@ -64,11 +65,38 @@ export class PlaywrightTraceService {
64
65
  }
65
66
  return this._backend.callTool(name, args);
66
67
  }
68
+ /**
69
+ * How long shutdown() waits for the fork's session-end teardown, so a hung
70
+ * target cannot hold the process forever. Teardown runs after a 5 s
71
+ * body-read settle, in up to 3 DELETE passes; each DELETE times out after
72
+ * 10 s, and a pass in which every DELETE timed out is not retried. Several
73
+ * hung DELETEs can still use up this bound. The fork writes the ledger after
74
+ * each deletion, so the ledger file is still correct when the bound runs out.
75
+ */
76
+ static shutdownTeardownTimeoutMs = 30_000;
77
+ /**
78
+ * The fork's serverClosed() starts data-safety teardown and returns at
79
+ * once. The caller exits the process right after this resolves, so wait
80
+ * for that work (bounded) — or the teardown DELETEs never reach the target.
81
+ */
67
82
  async shutdown() {
68
83
  if (this._backend) {
69
- this._backend.serverClosed?.();
84
+ const backend = this._backend;
70
85
  this._backend = null;
71
86
  this._tools = [];
87
+ backend.serverClosed?.();
88
+ const pending = backend.autoExportPromise;
89
+ if (pending) {
90
+ let timer;
91
+ const bound = new Promise((resolve) => {
92
+ timer = setTimeout(() => {
93
+ logger.warning(`Data-safety teardown did not finish within ${PlaywrightTraceService.shutdownTeardownTimeoutMs} ms; exiting anyway`);
94
+ resolve();
95
+ }, PlaywrightTraceService.shutdownTeardownTimeoutMs);
96
+ });
97
+ await Promise.race([pending.catch(() => { }), bound]);
98
+ clearTimeout(timer);
99
+ }
72
100
  logger.info("PlaywrightTraceService shut down");
73
101
  }
74
102
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Mcp-side data-safety config resolution + off-limits URL check.
3
+ *
4
+ * Resolution stack — which function a caller should reach for:
5
+ * - `resolveDataSafetyOptions` (this file): reads the workspace.yml
6
+ * `dataSafety` section, honoring the env-var precedence rule below.
7
+ * Lowest-level; used to build the fork's `options` argument.
8
+ * - `effectiveDataSafetyConfig` (this file): resolves through the fork's
9
+ * own `parseDataSafetyConfig` so the result matches exactly what the
10
+ * fork's network-layer guard will enforce. Can throw (unguarded require
11
+ * of the vendored fork).
12
+ * - `safeResolveDataSafety` (safeResolveDataSafety.ts): fail-open wrapper
13
+ * around `effectiveDataSafetyConfig`.
14
+ * - `resolveDataSafetyForRegistration` (registerPlaywrightTools.ts):
15
+ * fail-open wrapper for the enforcement lane specifically — resolves via
16
+ * `safeResolveDataSafety` and additionally normalizes an inert result
17
+ * (mode "off", no offLimits) to `null` for the navigate pre-check.
18
+ *
19
+ * The vendored Playwright fork's DataSafetyGuard/parseDataSafetyConfig read
20
+ * their config with precedence `options ?? env ?? inert`. Testbot resolves
21
+ * the full precedence chain (action input > workspace > auto-default) itself
22
+ * and ships the result via the SKYRAMP_DATA_SAFETY env var, which the fork
23
+ * reads directly. If mcp ALSO passed options derived from the workspace file
24
+ * in that lane, options would win over env and silently override testbot's
25
+ * resolution. So `resolveDataSafetyOptions` returns undefined whenever
26
+ * SKYRAMP_DATA_SAFETY is set — only the env-less lane (local/manual MCP
27
+ * usage, no testbot in the loop) reads the workspace section itself.
28
+ *
29
+ * The dataSafety section is read out of the raw YAML rather than through the
30
+ * full strict workspace schema (see workspace/workspace.ts), so an old or
31
+ * unrelated workspace.yml that fails full schema validation for reasons that
32
+ * have nothing to do with data safety cannot flip anyone to strict by
33
+ * accident. A PRESENT dataSafety section goes to the fork as written, and the
34
+ * fork's validateConfig checks each field on its own: a bad mode fails closed
35
+ * to strict, and a bad offLimits entry stops every tool call, so a bad field
36
+ * never drops the fields next to it. A workspace.yml that does not parse but
37
+ * has a top-level dataSafety key fails closed to strict.
38
+ */
39
+ export type DataSafetyConfig = {
40
+ mode: "strict" | "readonly" | "off";
41
+ offLimits: string[];
42
+ };
43
+ /**
44
+ * The fork's `options` argument: the workspace.yml dataSafety section as
45
+ * written, unvalidated. The fork's parseDataSafetyConfig validates it.
46
+ */
47
+ export type DataSafetyOptions = unknown;
48
+ /**
49
+ * Resolve mcp-side options for the trace backend. Returns undefined when
50
+ * SKYRAMP_DATA_SAFETY is set (testbot's resolution wins — see module
51
+ * docstring) or when no dataSafety signal exists in the workspace file.
52
+ * Only `mode` (absent means "off") and `offLimits` are passed on, so a field
53
+ * added to the section later cannot change what the fork enforces.
54
+ */
55
+ export declare function resolveDataSafetyOptions(workspaceRoot: string): Promise<DataSafetyOptions | undefined>;
56
+ /** The config exactly as the fork's parseDataSafetyConfig will resolve it. */
57
+ export declare function effectiveDataSafetyConfig(workspaceRoot: string): Promise<DataSafetyConfig>;
58
+ /** offLimits check via the fork's own pure matcher so mcp and fork can never disagree. */
59
+ export declare function isOffLimitsUrl(url: string, config: DataSafetyConfig): boolean;
60
+ /**
61
+ * Maps a resolved data-safety config to the `options` argument
62
+ * `registerPlaywrightTools` expects, for the enforcement-lane wiring in
63
+ * index.ts: `resolveDataSafetyOptions(cwd).then(ds => registerPlaywrightTools(server, playwrightToolOptionsFor(ds)))`.
64
+ * Extracted (rather than an inline arrow at the call site) so the mapping is
65
+ * directly unit-testable without booting the McpServer — mirrors
66
+ * `resolveDataSafetyForRegistration`'s existing test approach
67
+ * (registerPlaywrightTools.dataSafety.test.ts). The return type is left
68
+ * structural (not `PlaywrightTraceOptions`) to avoid a circular import with
69
+ * PlaywrightTraceService.ts, which itself imports `DataSafetyConfig` from
70
+ * this module.
71
+ */
72
+ export declare function playwrightToolOptionsFor(dataSafety: DataSafetyOptions | undefined): {
73
+ dataSafety: DataSafetyOptions;
74
+ } | undefined;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Mcp-side data-safety config resolution + off-limits URL check.
3
+ *
4
+ * Resolution stack — which function a caller should reach for:
5
+ * - `resolveDataSafetyOptions` (this file): reads the workspace.yml
6
+ * `dataSafety` section, honoring the env-var precedence rule below.
7
+ * Lowest-level; used to build the fork's `options` argument.
8
+ * - `effectiveDataSafetyConfig` (this file): resolves through the fork's
9
+ * own `parseDataSafetyConfig` so the result matches exactly what the
10
+ * fork's network-layer guard will enforce. Can throw (unguarded require
11
+ * of the vendored fork).
12
+ * - `safeResolveDataSafety` (safeResolveDataSafety.ts): fail-open wrapper
13
+ * around `effectiveDataSafetyConfig`.
14
+ * - `resolveDataSafetyForRegistration` (registerPlaywrightTools.ts):
15
+ * fail-open wrapper for the enforcement lane specifically — resolves via
16
+ * `safeResolveDataSafety` and additionally normalizes an inert result
17
+ * (mode "off", no offLimits) to `null` for the navigate pre-check.
18
+ *
19
+ * The vendored Playwright fork's DataSafetyGuard/parseDataSafetyConfig read
20
+ * their config with precedence `options ?? env ?? inert`. Testbot resolves
21
+ * the full precedence chain (action input > workspace > auto-default) itself
22
+ * and ships the result via the SKYRAMP_DATA_SAFETY env var, which the fork
23
+ * reads directly. If mcp ALSO passed options derived from the workspace file
24
+ * in that lane, options would win over env and silently override testbot's
25
+ * resolution. So `resolveDataSafetyOptions` returns undefined whenever
26
+ * SKYRAMP_DATA_SAFETY is set — only the env-less lane (local/manual MCP
27
+ * usage, no testbot in the loop) reads the workspace section itself.
28
+ *
29
+ * The dataSafety section is read out of the raw YAML rather than through the
30
+ * full strict workspace schema (see workspace/workspace.ts), so an old or
31
+ * unrelated workspace.yml that fails full schema validation for reasons that
32
+ * have nothing to do with data safety cannot flip anyone to strict by
33
+ * accident. A PRESENT dataSafety section goes to the fork as written, and the
34
+ * fork's validateConfig checks each field on its own: a bad mode fails closed
35
+ * to strict, and a bad offLimits entry stops every tool call, so a bad field
36
+ * never drops the fields next to it. A workspace.yml that does not parse but
37
+ * has a top-level dataSafety key fails closed to strict.
38
+ */
39
+ import fs from "fs";
40
+ import path from "path";
41
+ import { createRequire } from "module";
42
+ import yaml from "js-yaml";
43
+ import { logger } from "../utils/logger.js";
44
+ const require = createRequire(import.meta.url);
45
+ const TOP_LEVEL_DATA_SAFETY_KEY = /^["']?dataSafety["']?\s*:/m;
46
+ /**
47
+ * Resolve mcp-side options for the trace backend. Returns undefined when
48
+ * SKYRAMP_DATA_SAFETY is set (testbot's resolution wins — see module
49
+ * docstring) or when no dataSafety signal exists in the workspace file.
50
+ * Only `mode` (absent means "off") and `offLimits` are passed on, so a field
51
+ * added to the section later cannot change what the fork enforces.
52
+ */
53
+ export async function resolveDataSafetyOptions(workspaceRoot) {
54
+ if (process.env.SKYRAMP_DATA_SAFETY?.trim())
55
+ return undefined;
56
+ const configPath = path.join(workspaceRoot, ".skyramp", "workspace.yml");
57
+ let text;
58
+ try {
59
+ text = fs.readFileSync(configPath, "utf8");
60
+ }
61
+ catch {
62
+ return undefined; // no file: no signal
63
+ }
64
+ let raw;
65
+ try {
66
+ raw = yaml.load(text);
67
+ }
68
+ catch (error) {
69
+ if (!TOP_LEVEL_DATA_SAFETY_KEY.test(text))
70
+ return undefined;
71
+ logger.warning("workspace.yml is not valid YAML and has a dataSafety section — failing closed to strict, with no offLimits", { error: error instanceof Error ? error.message : String(error) });
72
+ return { mode: "strict" };
73
+ }
74
+ const section = raw?.["dataSafety"];
75
+ if (section === undefined || section === null)
76
+ return undefined;
77
+ if (typeof section !== "object" || Array.isArray(section))
78
+ return section;
79
+ const { mode, offLimits } = section;
80
+ return { mode: mode === undefined ? "off" : mode, offLimits };
81
+ }
82
+ /** The config exactly as the fork's parseDataSafetyConfig will resolve it. */
83
+ export async function effectiveDataSafetyConfig(workspaceRoot) {
84
+ const options = await resolveDataSafetyOptions(workspaceRoot);
85
+ const { parseDataSafetyConfig } = require("playwright/lib/mcp/skyramp");
86
+ return parseDataSafetyConfig(options, process.env.SKYRAMP_DATA_SAFETY);
87
+ }
88
+ /** offLimits check via the fork's own pure matcher so mcp and fork can never disagree. */
89
+ export function isOffLimitsUrl(url, config) {
90
+ if (!config.offLimits.length)
91
+ return false;
92
+ const { matchesOffLimits } = require("playwright/lib/mcp/skyramp");
93
+ return matchesOffLimits(url, config.offLimits);
94
+ }
95
+ /**
96
+ * Maps a resolved data-safety config to the `options` argument
97
+ * `registerPlaywrightTools` expects, for the enforcement-lane wiring in
98
+ * index.ts: `resolveDataSafetyOptions(cwd).then(ds => registerPlaywrightTools(server, playwrightToolOptionsFor(ds)))`.
99
+ * Extracted (rather than an inline arrow at the call site) so the mapping is
100
+ * directly unit-testable without booting the McpServer — mirrors
101
+ * `resolveDataSafetyForRegistration`'s existing test approach
102
+ * (registerPlaywrightTools.dataSafety.test.ts). The return type is left
103
+ * structural (not `PlaywrightTraceOptions`) to avoid a circular import with
104
+ * PlaywrightTraceService.ts, which itself imports `DataSafetyConfig` from
105
+ * this module.
106
+ */
107
+ export function playwrightToolOptionsFor(dataSafety) {
108
+ return dataSafety !== undefined ? { dataSafety } : undefined;
109
+ }
@@ -0,0 +1 @@
1
+ export declare const ESSENTIAL_TOOLS: ReadonlySet<string>;
@@ -0,0 +1,44 @@
1
+ // Only expose essential browser tools to reduce LLM confusion.
2
+ // The full set (23 tools) overwhelms the LLM and causes it to use
3
+ // browser_evaluate/browser_run_code instead of the simpler tools.
4
+ //
5
+ // browser_blueprint is the only DOM Analyzer tool registered — it handles
6
+ // both full captures and same-URL deltas via the URL-cache (isFullCapture).
7
+ // browser_blueprint_diff, browser_sitemap_build, browser_sitemap_query fire
8
+ // 0 times in production evals and have no active call path in the testbot prompt.
9
+ //
10
+ // browser_widget_contract_lookup / browser_widget_contract_cache are
11
+ // intentionally excluded: the curated fingerprint library only covers
12
+ // test fixtures (not production widgets), and the LLM can handle custom
13
+ // widgets via snapshot-driven trial-and-error without the tool.
14
+ // The widgetType field on blueprint elements still signals "custom" to
15
+ // the agent — no guidance is lost.
16
+ export const ESSENTIAL_TOOLS = new Set([
17
+ "browser_navigate",
18
+ "browser_snapshot",
19
+ "browser_click",
20
+ "browser_type",
21
+ "browser_press_key",
22
+ "browser_select_option",
23
+ "browser_hover",
24
+ "browser_drag",
25
+ "browser_file_upload",
26
+ "browser_evaluate",
27
+ "browser_tabs",
28
+ "browser_navigate_back",
29
+ "browser_wait_for",
30
+ "browser_take_screenshot",
31
+ "browser_assert",
32
+ "browser_assert_hidden",
33
+ "browser_assert_api_request",
34
+ "browser_assert_table_cell",
35
+ "browser_mouse_action",
36
+ "browser_visual_snapshot",
37
+ "browser_gojs_add_node",
38
+ "browser_gojs_link",
39
+ "skyramp_export_zip",
40
+ "skyramp_load_trace",
41
+ "skyramp_finish_test_setup",
42
+ // DOM Analyzer: blueprint capture (capture-act-capture pattern)
43
+ "browser_blueprint",
44
+ ]);
@@ -3,6 +3,41 @@
3
3
  * tools in the Skyramp MCP server, delegating calls to PlaywrightTraceService.
4
4
  */
5
5
  import { PlaywrightTraceService, type PlaywrightTraceOptions } from "./PlaywrightTraceService.js";
6
- import type { McpServer } from "@modelcontextprotocol/server";
6
+ import type { DataSafetyConfig } from "./dataSafetyConfig.js";
7
+ import type { McpServer, CallToolResult } from "@modelcontextprotocol/server";
7
8
  export declare function getPlaywrightTraceService(): PlaywrightTraceService | null;
9
+ /** Courtesy pre-check so the agent gets an immediate, readable refusal
10
+ * instead of a page-level network error. The fork's network layer remains
11
+ * the hard guarantee — this is UX, not enforcement — so a throw from the
12
+ * matcher (e.g. the vendored fork's require() resolving a partial install)
13
+ * must degrade to "not off-limits" rather than block navigation outright. */
14
+ export declare function precheckNavigate(url: string, config: DataSafetyConfig): string | null;
15
+ /**
16
+ * Pure regression guard for the per-call browser_navigate pre-check: the
17
+ * `tool.name === "browser_navigate" && config` gate, the params.url
18
+ * extraction, and the isError result shape, all extracted out of the tool
19
+ * handler closure so they can be exercised directly under Jest without a
20
+ * real McpServer/PlaywrightTraceService. The registration call site becomes
21
+ * `const refusal = maybeRefuseNavigate(...); if (refusal) return refusal;`.
22
+ * Returns undefined for every "let the call through" case: wrong tool, no
23
+ * url param, or no config (data safety not configured for this run).
24
+ */
25
+ export declare function maybeRefuseNavigate(toolName: string, params: unknown, config: DataSafetyConfig | undefined): CallToolResult | undefined;
26
+ /**
27
+ * Resolves the data-safety pre-check config for `registerPlaywrightTools`
28
+ * and normalizes an inert result (mode "off" with
29
+ * no offLimits) to `null`. MUST go through `safeResolveDataSafety` — this
30
+ * runs after `_service.initialize()` has already succeeded, so an unguarded
31
+ * rejection here (e.g. the vendored fork's `require()` resolving a partial
32
+ * install) would abort registration of all 23 Playwright browser tools, not
33
+ * just the advisory pre-check. The inner try/catch is defense in depth in
34
+ * case `safeResolveDataSafety` itself ever regresses its no-throw contract —
35
+ * either way, an undefined/failed resolution degrades to "pre-check inert"
36
+ * (`null`), never a thrown error.
37
+ *
38
+ * Exported (rather than inlined) so it can be exercised directly under Jest
39
+ * without driving the McpServer-dependent registration flow — see
40
+ * `registerPlaywrightTools.dataSafety.test.ts`.
41
+ */
42
+ export declare function resolveDataSafetyForRegistration(cwd: string): Promise<DataSafetyConfig | null>;
8
43
  export declare function registerPlaywrightTools(server: McpServer, options?: PlaywrightTraceOptions): Promise<void>;