Skip to content

feat(playground): compact runtime execution results#2128

Draft
quanru wants to merge 3 commits intomainfrom
feat/playground-compact-execution-results
Draft

feat(playground): compact runtime execution results#2128
quanru wants to merge 3 commits intomainfrom
feat/playground-compact-execution-results

Conversation

@quanru
Copy link
Collaborator

@quanru quanru commented Mar 12, 2026

Summary

  • return live execution dump plus compact snapshot from local execution results instead of generating runtime report HTML
  • keep remote progress on compact snapshots and limit final server responses to a single replay dump without report file generation
  • switch unstable YAML log content to compact snapshots and add regression coverage for the new runtime execution data path

Validation

  • pnpm --filter @midscene/core test -- tests/unit-test/agent-dump-update.test.ts
  • npx nx test @midscene/playground
  • npx nx build @midscene/core
  • npx nx build @midscene/playground
  • npx nx build @midscene/visualizer
  • pnpm run lint

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 12, 2026

Deploying midscene with  Cloudflare Pages  Cloudflare Pages

Latest commit: de15877
Status: ✅  Deploy successful!
Preview URL: https://4506a939.midscene.pages.dev
Branch Preview URL: https://feat-playground-compact-exec.midscene.pages.dev

View logs

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46d695f693

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +180 to +182
const liveExecutionDump = this.agent?.dump?.executions?.[0];
if (liveExecutionDump) {
return liveExecutionDump;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize local execution dump before returning it

readLiveExecutionDump now returns this.agent.dump.executions[0] directly, which keeps the raw ExecutionTask objects from TaskRunner.dump() (including fields like executor functions and other non-serializable runtime data). In local mode, usePlaygroundExecution persists results via the default IndexedDB provider, and IndexedDBStorageProvider.saveResult writes the whole dump object through dbManager.put; structured cloning fails on function-valued fields (DataCloneError), so execution history is no longer persisted for local runs. Returning a serialized copy (as before via dumpDataString parsing) avoids this regression.

Useful? React with 👍 / 👎.

@quanru quanru changed the base branch from feat/core-runtime-dump-decoupling to main March 13, 2026 03:05
@quanru quanru marked this pull request as draft March 13, 2026 03:46
@quanru quanru force-pushed the feat/playground-compact-execution-results branch from 8c51574 to de15877 Compare March 16, 2026 02:43
artifact?.absolutePath ??
join(this.screenshotsDir, `${ref.id}.${ref.format}`);

const buffer = readFileSync(absolutePath);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 3 days ago

In general, to fix this kind of issue you must validate or constrain user-controlled data before using it to build a filesystem path. For a case like this, where the server should only ever access images that are part of the current run’s screenshot artifacts, the safest approach is: (1) ensure all file reads go through IDs that are resolved against a known in-memory artifact registry, and (2) when falling back to constructing a path, only allow simple, sanitized filenames (no directory separators) and ensure the final absolute path stays under a known screenshots root.

Concretely, the critical path is in RuntimeArtifactStore.resolveImage in packages/core/src/dump/runtime-artifact-store.ts, where absolutePath is set based on the (tainted) ref.id and ref.format. The best minimally invasive fix is to: (a) validate that ref looks like a legitimate screenshot reference using the existing isScreenshotArtifactRef function, (b) if there is an artifact entry for ref.id, use its absolutePath but also ensure that path is under this.screenshotsDir, and (c) if there is no artifact entry, sanitize ref.id so that it cannot contain path separators, build a candidate path under this.screenshotsDir, resolve it to an absolute path with resolve, and verify that the resolved path starts with the canonical screenshots directory path. If any of these checks fail, throw an error instead of reading the file.

To implement this, we only need to modify resolveImage in packages/core/src/dump/runtime-artifact-store.ts and add an import of resolve from node:path in that file. We will reuse the existing isScreenshotArtifactRef export from the same module to validate the ref. The rest of the system’s behavior stays the same: callers still pass a ScreenshotArtifactRef, and successful calls still return a base64-encoded data URL. The new checks simply prevent directory traversal or arbitrary file access via malicious id or artifact.absolutePath values.

Suggested changeset 1
packages/core/src/dump/runtime-artifact-store.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/core/src/dump/runtime-artifact-store.ts b/packages/core/src/dump/runtime-artifact-store.ts
--- a/packages/core/src/dump/runtime-artifact-store.ts
+++ b/packages/core/src/dump/runtime-artifact-store.ts
@@ -6,7 +6,7 @@
   rmSync,
   writeFileSync,
 } from 'node:fs';
-import { join } from 'node:path';
+import { join, resolve } from 'node:path';
 import { getMidsceneRunSubDir } from '@midscene/shared/common';
 import { uuid } from '@midscene/shared/utils';
 import { ScreenshotItem } from '../screenshot-item';
@@ -206,11 +206,33 @@
   }
 
   async resolveImage(ref: ScreenshotArtifactRef): Promise<string> {
+    // Basic shape validation to avoid obviously malformed refs
+    if (!isScreenshotArtifactRef(ref)) {
+      throw new Error('Invalid screenshot artifact reference');
+    }
+
+    // Resolve screenshots directory to an absolute, normalized path
+   const screenshotsRoot = resolve(this.screenshotsDir);
+
+    let candidatePath: string | undefined;
     const artifact = this.artifacts.get(ref.id);
-    const absolutePath =
-      artifact?.absolutePath ??
-      join(this.screenshotsDir, `${ref.id}.${ref.format}`);
 
+    if (artifact?.absolutePath) {
+      candidatePath = artifact.absolutePath;
+    } else {
+      // Disallow path separators in the ID to prevent directory traversal
+      const safeId = ref.id.replace(/[\\/]/g, '');
+      candidatePath = join(screenshotsRoot, `${safeId}.${ref.format}`);
+    }
+
+    const absolutePath = resolve(candidatePath);
+
+    // Ensure the resolved path is contained within the screenshots root
+    if (!absolutePath.startsWith(screenshotsRoot + '/') &&
+        absolutePath !== screenshotsRoot) {
+      throw new Error('Screenshot path is outside of the allowed directory');
+    }
+
     const buffer = readFileSync(absolutePath);
     return `data:image/${ref.format};base64,${buffer.toString('base64')}`;
   }
EOF
@@ -6,7 +6,7 @@
rmSync,
writeFileSync,
} from 'node:fs';
import { join } from 'node:path';
import { join, resolve } from 'node:path';
import { getMidsceneRunSubDir } from '@midscene/shared/common';
import { uuid } from '@midscene/shared/utils';
import { ScreenshotItem } from '../screenshot-item';
@@ -206,11 +206,33 @@
}

async resolveImage(ref: ScreenshotArtifactRef): Promise<string> {
// Basic shape validation to avoid obviously malformed refs
if (!isScreenshotArtifactRef(ref)) {
throw new Error('Invalid screenshot artifact reference');
}

// Resolve screenshots directory to an absolute, normalized path
const screenshotsRoot = resolve(this.screenshotsDir);

let candidatePath: string | undefined;
const artifact = this.artifacts.get(ref.id);
const absolutePath =
artifact?.absolutePath ??
join(this.screenshotsDir, `${ref.id}.${ref.format}`);

if (artifact?.absolutePath) {
candidatePath = artifact.absolutePath;
} else {
// Disallow path separators in the ID to prevent directory traversal
const safeId = ref.id.replace(/[\\/]/g, '');
candidatePath = join(screenshotsRoot, `${safeId}.${ref.format}`);
}

const absolutePath = resolve(candidatePath);

// Ensure the resolved path is contained within the screenshots root
if (!absolutePath.startsWith(screenshotsRoot + '/') &&
absolutePath !== screenshotsRoot) {
throw new Error('Screenshot path is outside of the allowed directory');
}

const buffer = readFileSync(absolutePath);
return `data:image/${ref.format};base64,${buffer.toString('base64')}`;
}
Copilot is powered by AI and may make mistakes. Always verify output.
return;
}

state.event = payload.event;

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
user controlled input
.

Copilot Autofix

AI 3 days ago

In general, to fix prototype-polluting assignments you must ensure that untrusted strings are not used directly as property names on plain objects that inherit from Object.prototype. This can be done in two main ways: (1) use a data structure that is not affected by prototype keys (like Map or an object created via Object.create(null)), or (2) validate/restrict keys to disallow dangerous values like "__proto__", "constructor", and "prototype".

For this specific code, the problematic part is using requestId (from req.body) as an index into this.taskExecutionState[requestId] and then treating the result as a mutable state object. The minimal, behavior-preserving fix is to ensure that this.taskExecutionState is a prototype-less object, so that even if requestId is "__proto__" it will not resolve to Object.prototype. We can do this by initializing this.taskExecutionState with Object.create(null) inside the same file. Since we are allowed to edit only shown snippets in packages/playground/src/server.ts, we insert an initialization of taskExecutionState as a field on the containing class (or object) using Object.create(null) where its declaration is present in this file. If the declaration is not visible in the snippet, we can still safely add an initialization in the closest visible location where this is the class instance (for example, in the constructor), but we must stay within the given file.

Concretely:

  • Locate where taskExecutionState is defined/initialized in packages/playground/src/server.ts (most likely as a field of the server class).
  • Replace its initialization from {} (or an uninitialized field) to Object.create(null).
  • If taskExecutionState is currently not initialized at all in the file, add an explicit initialization, e.g. this.taskExecutionState = Object.create(null); in the constructor or as a class field initializer.
  • No additional imports are needed because Object.create is built-in.

This change preserves existing semantics (it is still a plain “dictionary” of task states) but removes the Object.prototype from its prototype chain, eliminating prototype pollution via requestId.


Suggested changeset 1
packages/playground/src/server.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/playground/src/server.ts b/packages/playground/src/server.ts
--- a/packages/playground/src/server.ts
+++ b/packages/playground/src/server.ts
@@ -26,6 +26,15 @@
 
 const defaultPort = PLAYGROUND_SERVER_PORT;
 
+/**
+ * Task execution state storage.
+ * Use a prototype-less object to avoid prototype pollution when indexing with untrusted keys.
+ */
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore - initialized here if not already declared as a class field
+(ServerImpl as unknown as { prototype: { taskExecutionState?: Record<string, unknown> } }).prototype.taskExecutionState ??=
+  Object.create(null);
+
 // Static path for playground files
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = dirname(__filename);
EOF
@@ -26,6 +26,15 @@

const defaultPort = PLAYGROUND_SERVER_PORT;

/**
* Task execution state storage.
* Use a prototype-less object to avoid prototype pollution when indexing with untrusted keys.
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - initialized here if not already declared as a class field
(ServerImpl as unknown as { prototype: { taskExecutionState?: Record<string, unknown> } }).prototype.taskExecutionState ??=
Object.create(null);

// Static path for playground files
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Copilot is powered by AI and may make mistakes. Always verify output.
}

state.event = payload.event;
state.snapshot = payload.getSnapshot();

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
user controlled input
.

Copilot Autofix

AI 3 days ago

In general, to fix prototype-polluting assignments you must avoid using untrusted strings as property names on plain objects that inherit from Object.prototype. You can either (1) switch to a safe associative structure like Map or a prototype-less object, or (2) validate or mangle untrusted keys so they cannot be __proto__, constructor, prototype, or otherwise collide with built-in properties.

For this specific code, the minimal, behavior-preserving fix is to validate requestId before using it as a key into this.taskExecutionState and to reject dangerous values. The vulnerable access is at line 474: this.taskExecutionState[requestId] = { ... }, and the subsequent use at line 482: const state = this.taskExecutionState[requestId];. We can add a short guard right after destructuring the body, or immediately before the first use of requestId as an index, that checks for forbidden property names. If the requestId is missing or equals __proto__, prototype, or constructor, we return an HTTP 400 error. This keeps the overall functionality (one task per requestId) intact while eliminating the possibility of writing to Object.prototype.

Concretely:

  • In packages/playground/src/server.ts, inside the /execute handler, after destructuring requestId from req.body and before line 471–474 where this.currentTaskId and this.taskExecutionState[requestId] are set, add a validation block that:
    • If requestId is not provided, returns a 400 error (if that’s considered required; if it’s optional, just skip the polluted keys check when it’s falsy).
    • If requestId is one of __proto__, prototype, or constructor, returns a 400 error.
  • This requires no new imports or type changes and does not alter behavior for valid IDs.
Suggested changeset 1
packages/playground/src/server.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/playground/src/server.ts b/packages/playground/src/server.ts
--- a/packages/playground/src/server.ts
+++ b/packages/playground/src/server.ts
@@ -422,6 +422,16 @@
         });
       }
 
+      if (
+        requestId === '__proto__' ||
+        requestId === 'constructor' ||
+        requestId === 'prototype'
+      ) {
+        return res.status(400).json({
+          error: 'invalid requestId',
+        });
+      }
+
       // Recreate agent only when AI config has changed (via /config API)
       if (this.agentFactory && this._configDirty) {
         this._configDirty = false;
EOF
@@ -422,6 +422,16 @@
});
}

if (
requestId === '__proto__' ||
requestId === 'constructor' ||
requestId === 'prototype'
) {
return res.status(400).json({
error: 'invalid requestId',
});
}

// Recreate agent only when AI config has changed (via /config API)
if (this.agentFactory && this._configDirty) {
this._configDirty = false;
Copilot is powered by AI and may make mistakes. Always verify output.
state.event = payload.event;
state.snapshot = payload.getSnapshot();
if (payload.event.type === 'execution_updated') {
state.executionDump = payload.event

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
user controlled input
.

Copilot Autofix

AI 3 days ago

In general, to fix prototype-polluting assignments you must ensure untrusted strings are not used directly as property names on plain objects that inherit from Object.prototype. You can do this by: (1) validating and rejecting dangerous keys ("__proto__", "prototype", "constructor"), (2) prefixing keys so they cannot collide with special properties, or (3) using safe structures like Map or Object.create(null).

For this specific case, the minimal, non-breaking fix is to validate requestId before it is used as an index into this.taskExecutionState and before any callbacks that rely on it are registered. Right after destructuring requestId from req.body and before checking this.currentTaskId, we can add a guard that rejects a malformed or dangerous requestId (including __proto__, prototype, constructor). If the requestId is missing or invalid, we should respond with a 400 error and not continue, preventing it from ever being used as a key in this.taskExecutionState or for other internal lookups. This requires no new imports and does not alter existing behavior for valid IDs; it only adds rejection of clearly invalid values that could lead to prototype pollution.

Concretely:

  • In packages/playground/src/server.ts, within the /execute handler, after extracting requestId from req.body, add a block that:
    • If requestId is defined but not a non-empty string, rejects with 400.
    • If requestId equals "__proto__", "prototype", or "constructor", rejects with 400.
  • This ensures that the subsequent usage at lines 473–482 cannot receive a prototype-polluting key and makes the CodeQL alert go away.
Suggested changeset 1
packages/playground/src/server.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/playground/src/server.ts b/packages/playground/src/server.ts
--- a/packages/playground/src/server.ts
+++ b/packages/playground/src/server.ts
@@ -422,6 +422,27 @@
         });
       }
 
+      // Validate requestId to prevent prototype pollution via special property names
+      if (
+        requestId !== undefined &&
+        requestId !== null &&
+        typeof requestId !== 'string'
+      ) {
+        return res.status(400).json({
+          error: 'requestId must be a string when provided',
+        });
+      }
+
+      if (
+        requestId === '__proto__' ||
+        requestId === 'prototype' ||
+        requestId === 'constructor'
+      ) {
+        return res.status(400).json({
+          error: 'invalid requestId',
+        });
+      }
+
       // Recreate agent only when AI config has changed (via /config API)
       if (this.agentFactory && this._configDirty) {
         this._configDirty = false;
EOF
@@ -422,6 +422,27 @@
});
}

// Validate requestId to prevent prototype pollution via special property names
if (
requestId !== undefined &&
requestId !== null &&
typeof requestId !== 'string'
) {
return res.status(400).json({
error: 'requestId must be a string when provided',
});
}

if (
requestId === '__proto__' ||
requestId === 'prototype' ||
requestId === 'constructor'
) {
return res.status(400).json({
error: 'invalid requestId',
});
}

// Recreate agent only when AI config has changed (via /config API)
if (this.agentFactory && this._configDirty) {
this._configDirty = false;
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant