Skip to content

feat: include DimSim#2081

Open
paul-nechifor wants to merge 2 commits into
mainfrom
paul/feat/include-dimsim
Open

feat: include DimSim#2081
paul-nechifor wants to merge 2 commits into
mainfrom
paul/feat/include-dimsim

Conversation

@paul-nechifor
Copy link
Copy Markdown
Contributor

@paul-nechifor paul-nechifor commented May 14, 2026

Problem

Closes DIM-XXX

Solution

How to Test

Contributor License Agreement

  • I have read and approved the CLA.

sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
except AttributeError:
pass
sock.bind(("", MCAST_PORT))
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.bind(("", LCM_PORT))
Comment thread misc/DimSim/server.js
Comment on lines +146 to +156
app.get("/vlm/asset-library", (req, res) => {
try {
if (existsSync(ASSET_LIBRARY_FILE)) {
const data = JSON.parse(readFileSync(ASSET_LIBRARY_FILE, "utf-8"));
if (Array.isArray(data)) return res.json({ assets: data });
}
} catch (err) {
console.error("[asset-library] read failed:", err.message);
}
res.json({ assets: [] });
});
Comment thread misc/DimSim/server.js
Comment on lines +159 to +168
app.post("/vlm/asset-library", (req, res) => {
try {
const { assets } = req.body;
writeFileSync(ASSET_LIBRARY_FILE, JSON.stringify(assets), "utf-8");
res.json({ ok: true, count: assets?.length || 0 });
} catch (err) {
console.error("[asset-library] write failed:", err.message);
res.status(500).json({ detail: err.message });
}
});
_npcClock: any = null; // THREE.Clock

async _execCode(code: string, id?: string): Promise<void> {
console.log(`[sceneEditor] exec${id ? ` (${id})` : ""}:`, code.slice(0, 100));
}

async _loadScript(url: string, id?: string): Promise<void> {
console.log(`[sceneEditor] loadScript${id ? ` (${id})` : ""}:`, url);
async _loadScript(url: string, id?: string): Promise<void> {
console.log(`[sceneEditor] loadScript${id ? ` (${id})` : ""}:`, url);
try {
const resp = await fetch(url);
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 14, 2026

Greptile Summary

This PR introduces DimSim, a 3D browser-based robotics simulator, as a new subproject under misc/DimSim/, along with a Deno CLI toolchain, a Playwright-based headless eval runner, a WebSocket bridge server, and a Python DimSimProcess wrapper that integrates DimSim into the existing dimos simulation stack.

  • Bridge server & frontend injection (bridge/server.ts:319): the scene CLI flag is string-interpolated directly into an inline <script> block without escaping; a scene name containing " causes a JS syntax error, and one containing </script> breaks the script tag entirely. Using JSON.stringify fixes this unconditionally.
  • Known open issues (flagged in prior review rounds, not yet resolved): the VLM proxy server (server.js) uses a wildcard CORS origin; extractGz in setup.ts passes user-supplied paths through sh -c without quoting; timed-out sendAndWait calls in eval/runner.ts leak WebSocket event listeners; and JUnit XML output in runner.ts does not escape special characters.

Confidence Score: 3/5

The PR introduces a large new subproject with several correctness and security issues, some unresolved from a prior round of review; merging while those issues remain open carries real risk.

The scene name is inserted into the served HTML page's inline script without escaping, which will silently corrupt the page for any scene name containing a double-quote, and allows tag injection for names containing </script>. Combined with the three previously-flagged but still-open issues — wildcard CORS on the VLM server, shell injection in extractGz, and WebSocket listener leaks in the eval runner — the change has a cluster of unresolved defects that collectively represent meaningful risk for a subproject being introduced into the main repo.

misc/DimSim/dimos-cli/bridge/server.ts (scene injection), misc/DimSim/server.js (CORS), and misc/DimSim/dimos-cli/setup.ts (shell injection) all need attention before this lands.

Security Review

  • misc/DimSim/dimos-cli/bridge/server.ts:319 — scene name injected into an inline <script> tag without HTML/JS escaping; a value containing </script> permits HTML injection into the served page.
  • misc/DimSim/server.js:68 — wildcard CORS (Access-Control-Allow-Origin: *) allows any website to invoke the local VLM proxy and trigger paid API calls using the server's stored keys (previously flagged, not yet resolved).
  • misc/DimSim/dimos-cli/setup.ts:121extractGz passes archive/destination paths via sh -c string interpolation; a crafted scene name could inject arbitrary shell commands (previously flagged, not yet resolved).

Important Files Changed

Filename Overview
misc/DimSim/dimos-cli/bridge/server.ts Central WebSocket bridge and static file server; scene name is injected into the served HTML without escaping, breaking the page for any scene name containing a double quote.
misc/DimSim/dimos-cli/eval/runner.ts Sequential and parallel eval runners; timed-out sendAndWait calls leak message-event listeners (previously flagged), and JUnit XML output does not escape special characters in rubric fields (previously flagged).
misc/DimSim/server.js Express VLM proxy server; CORS wildcard origin exposes API keys to any origin (previously flagged); otherwise straightforward request routing with retry logic.
misc/DimSim/dimos-cli/setup.ts Scene download and extraction logic; extractGz uses shell interpolation of user-supplied archive/destination paths (previously flagged); core download and version tracking logic is sound.
dimos/simulation/dimsim/dimsim_process.py Python wrapper that spawns the Deno CLI as a subprocess; lifecycle management is clean with daemon threads for log streaming and graceful SIGTERM/SIGKILL fallback in stop().
misc/DimSim/src/dimos/evalHarness.ts Browser-side eval orchestrator; rubric scoring is consistent with the runner's pass logic, and the ping/pong handshake and workflow lifecycle are correctly implemented.
.github/workflows/dimsim-check.yml New CI workflow for DimSim; runs npm build, Deno type-check, and scene template validation on changes to misc/DimSim/; permissions are correctly scoped to contents:read.
misc/DimSim/dimos-cli/headless/launcher.ts Playwright-based Chromium launcher supporting single-page and multi-page parallel eval modes; GPU/CPU render paths are cleanly separated with appropriate Chrome flags.

Sequence Diagram

sequenceDiagram
    participant PY as Python DimSimProcess
    participant CLI as Deno CLI (cli.ts)
    participant BRIDGE as Bridge Server (server.ts)
    participant BROWSER as Playwright/Browser (engine.js)
    participant LCM as LCM Multicast
    participant AGENT as Python Robot Agent

    PY->>CLI: spawn deno run cli.ts dev --scene apt --headless
    CLI->>BRIDGE: startBridgeServer(port, distDir, scene)
    CLI->>BROWSER: launchHeadless(url)
    BROWSER->>BRIDGE: WebSocket connect (control ch)
    BROWSER->>BRIDGE: WebSocket connect (sensor ch)
    BROWSER->>BRIDGE: Rapier snapshot (physics state)
    BRIDGE->>BRIDGE: initServerSystems(snapshot)
    BRIDGE->>LCM: LCM.start()

    AGENT->>LCM: publish /cmd_vel (velocity commands)
    LCM->>BRIDGE: packet received
    BRIDGE->>BROWSER: forward LCM packet via WS
    BROWSER->>BRIDGE: /odom (pose) via sensor WS
    BRIDGE->>LCM: republish odom

    Note over CLI,BROWSER: Eval mode
    CLI->>BROWSER: "sendAndWait({type:ping}, pong)"
    BROWSER-->>CLI: "{type:pong}"
    CLI->>BROWSER: "{type:startWorkflow, workflow}"
    BROWSER->>BROWSER: run rubric scoring at timeout
    BROWSER-->>CLI: "{type:workflowComplete, rubricScores}"
    CLI->>CLI: toJunitXml(results)
Loading

Reviews (3): Last reviewed commit: "skip lfs" | Re-trigger Greptile

Comment on lines +121 to +128
const proc = new Deno.Command("sh", {
args: ["-c", `gunzip -c "${archive}" > "${destFile}"`],
stdout: "inherit",
stderr: "inherit",
}).spawn();
const status = await proc.status;
if (!status.success) throw new Error(`gunzip failed (exit ${status.code})`);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Shell injection via user-supplied scene name

archive and destFile are derived directly from the CLI name argument and interpolated into a shell command string. A scene name containing " can escape the quoted argument and inject arbitrary shell commands — e.g. dimsim scene install 'apt"; rm -rf ~/; echo "' would execute the injected commands at the user's privilege level.

The fix is to avoid sh -c entirely and spawn gunzip with its arguments as a proper array, piping stdout to the destination file directly (e.g. Deno.Command("gunzip", { args: ["-c", archive], stdout: "piped" }) piped to a Deno.open write stream).

Comment on lines +374 to +384
for (const r of results) {
const time = (r.durationMs / 1000).toFixed(1);
xml += ` <testcase name="${r.name}" classname="${r.environment}" time="${time}"`;
if (r.pass) {
xml += ` />\n`;
} else {
xml += `>\n`;
xml += ` <failure message="${r.reason}">${JSON.stringify(r.rubricScores)}</failure>\n`;
xml += ` </testcase>\n`;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 XML special characters in r.name, r.environment, or r.reason (e.g. a task description containing " or <) will produce malformed JUnit XML, which many CI parsers will reject entirely. All values interpolated into XML attributes or content should be escaped.

Suggested change
for (const r of results) {
const time = (r.durationMs / 1000).toFixed(1);
xml += ` <testcase name="${r.name}" classname="${r.environment}" time="${time}"`;
if (r.pass) {
xml += ` />\n`;
} else {
xml += `>\n`;
xml += ` <failure message="${r.reason}">${JSON.stringify(r.rubricScores)}</failure>\n`;
xml += ` </testcase>\n`;
}
}
function escXml(s: string): string {
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
for (const r of results) {
const time = (r.durationMs / 1000).toFixed(1);
xml += ` <testcase name="${escXml(r.name)}" classname="${escXml(r.environment)}" time="${time}"`;
if (r.pass) {
xml += ` />\n`;
} else {
xml += `>\n`;
xml += ` <failure message="${escXml(r.reason)}">${escXml(JSON.stringify(r.rubricScores))}</failure>\n`;
xml += ` </testcase>\n`;
}
}

@codecov
Copy link
Copy Markdown

codecov Bot commented May 14, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

Co-authored-by: Viswajit Nair <viswajitnair@gmail.com>
@paul-nechifor paul-nechifor force-pushed the paul/feat/include-dimsim branch from 40287e9 to bed678f Compare May 14, 2026 05:00
Comment thread misc/DimSim/server.js

// ── App ─────────────────────────────────────────────────────────────────────
const app = express();
app.use(cors());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Wide-open CORS exposes local VLM API keys to any web page

cors() with no configuration sets Access-Control-Allow-Origin: *, which tells the browser to allow any website to make cross-origin requests to this server. Because the API keys (OPENAI_API_KEY, GEMINI_API_KEY) live server-side, a malicious web page the user happens to visit can silently send POST requests to http://127.0.0.1:8000/vlm/decision and http://127.0.0.1:8000/vlm/generate-image, triggering paid API calls on the user's behalf and reading the VLM responses. It can also overwrite the asset-library file via /vlm/asset-library.

Restrict the allowed origin to the DimSim frontend explicitly (e.g. cors({ origin: ["http://localhost:5173", "http://localhost:8080"] })) or, if the port is dynamic, validate the Origin header against a known prefix at request time.

Comment on lines +89 to +108
function sendAndWait(cmd: Record<string, unknown>, responseType: string, timeoutMs = 60000): Promise<Record<string, unknown>> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error(`Timeout waiting for ${responseType}`)), timeoutMs);

const handler = (event: MessageEvent) => {
if (typeof event.data !== "string") return;
try {
const msg = JSON.parse(event.data);
if (msg.type === responseType) {
clearTimeout(timeout);
ws.removeEventListener("message", handler);
resolve(msg);
}
} catch { /* not JSON */ }
};

ws.addEventListener("message", handler);
ws.send(JSON.stringify(cmd));
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Timed-out sendAndWait calls leak event listeners

When the timeout fires and the promise rejects, ws.removeEventListener("message", handler) is never called. During the ping/pong readiness loop (lines 115–124), each failed 3-second attempt registers another "pong" listener that is never cleaned up. After 60 retries (the full 60-second window), 60 stale handlers are attached to the WebSocket. They stay until a matching message finally arrives or the socket closes. In a long-running parallel eval session with repeated timeouts this grows unboundedly and could cause delayed message handling (all stale handlers fire on every incoming message).

The timeout callback should call ws.removeEventListener("message", handler) before calling reject(…) so the listener is always removed on settlement.

const ratesJs = sensorRates ? `window.__dimosSensorRates=${JSON.stringify(sensorRates)};` : "";
const enableJs = sensorEnable ? `window.__dimosSensorEnable=${JSON.stringify(sensorEnable)};` : "";
const fovJs = cameraFov ? `window.__dimosCameraFov=${cameraFov};` : "";
const inject = `<script>window.__dimosMode=true;window.__dimosScene="${scene || "apt"}";${headless ? "window.__dimosHeadless=true;" : ""}${ratesJs}${enableJs}${fovJs}</script>`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security The scene value is interpolated into a JS string literal using double-quote delimiters but is never escaped. A scene name containing " breaks the string (e.g. --scene 'apt" yields window.__dimosScene="apt";" which is a syntax error), and a value like apt"</script><script> would silently terminate the script block and inject arbitrary HTML. The idiomatic fix is to use JSON.stringify so the value is always a well-formed JS string literal, regardless of its contents.

Suggested change
const inject = `<script>window.__dimosMode=true;window.__dimosScene="${scene || "apt"}";${headless ? "window.__dimosHeadless=true;" : ""}${ratesJs}${enableJs}${fovJs}</script>`;
const inject = `<script>window.__dimosMode=true;window.__dimosScene=${JSON.stringify(scene || "apt")};${headless ? "window.__dimosHeadless=true;" : ""}${ratesJs}${enableJs}${fovJs}</script>`;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants