echoclaw-relay-agent 0.33.2 → 0.33.4

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.
@@ -3094,7 +3094,7 @@ var API_CAPABILITY_MAP = [
3094
3094
  { pattern: /navigator\.geolocation/g, capability: "location", label: "navigator.geolocation" },
3095
3095
  { pattern: /getDisplayMedia\s*\(/g, capability: "media_capture", label: "getDisplayMedia()" },
3096
3096
  { pattern: /requestFullscreen\s*\(/g, capability: "fullscreen", label: "requestFullscreen()" },
3097
- { pattern: /new\s+Audio\s*\(/g, capability: "audio", label: "new Audio()" },
3097
+ { pattern: /new\s+Audio\b/g, capability: "audio", label: "new Audio()" },
3098
3098
  { pattern: /<audio[\s>]/gi, capability: "audio", label: "<audio>" },
3099
3099
  { pattern: /AudioContext/g, capability: "audio", label: "AudioContext" },
3100
3100
  // Bridge APIs → capability
@@ -3109,9 +3109,9 @@ var PROMPT_TIER_CAPABILITIES = new Set(
3109
3109
  Object.entries(CAPABILITY_TIERS).filter(([, tier]) => tier === "prompt").map(([cap]) => cap)
3110
3110
  );
3111
3111
  var SIDEBAR_PATTERNS = [
3112
- /(?:class|id)\s*=\s*["'][^"']*\b(?:left-panel|left-sidebar|left-nav|left-sidenav)\b[^"']*["']/gi,
3113
- /\.(?:left-panel|left-sidebar|left-nav|left-sidenav)\b/gi,
3114
- /#(?:left-panel|left-sidebar|left-nav|left-sidenav)\b/gi
3112
+ /(?:class|id)\s*=\s*["'][^"']*\b(?:left-panel|left-sidebar|left-nav|left-sidenav|sidebar-left|panel-left|nav-left)\b[^"']*["']/gi,
3113
+ /\.(?:left-panel|left-sidebar|left-nav|left-sidenav|sidebar-left|panel-left|nav-left)\b/gi,
3114
+ /#(?:left-panel|left-sidebar|left-nav|left-sidenav|sidebar-left|panel-left|nav-left)\b/gi
3115
3115
  ];
3116
3116
  function validateClawHtml(html, manifest, options) {
3117
3117
  const phase = options?.phase ?? "hard";
@@ -3133,6 +3133,13 @@ function validateClawHtml(html, manifest, options) {
3133
3133
  if (/d\s*=\s*["'][^"']*$/.test(before)) continue;
3134
3134
  if (/href\s*=\s*["']$/.test(before)) continue;
3135
3135
  if (/id\s*=\s*["']$/.test(before)) continue;
3136
+ if (!isInStyleAttribute(html, hexMatch.index) && !isInScriptBlock(html, hexMatch.index)) {
3137
+ const afterHex = html.slice(hexMatch.index + fullHex.length, hexMatch.index + fullHex.length + 60);
3138
+ if (/^\s*[\{,\s:>+~\[]/.test(afterHex) || /^\s*$/.test(afterHex)) {
3139
+ const beforeCtx = html.slice(Math.max(0, hexMatch.index - 10), hexMatch.index);
3140
+ if (!/[:;]\s*$/.test(beforeCtx)) continue;
3141
+ }
3142
+ }
3136
3143
  const beforeFull = html.slice(Math.max(0, hexMatch.index - 500), hexMatch.index);
3137
3144
  const lastCommentOpen = beforeFull.lastIndexOf("<!--");
3138
3145
  const lastCommentClose = beforeFull.lastIndexOf("-->");
@@ -3241,6 +3248,36 @@ function validateClawHtml(html, manifest, options) {
3241
3248
  line: lineAt(html, linkMatch.index)
3242
3249
  });
3243
3250
  }
3251
+ const fetchUrlPattern = /\bfetch\s*\(\s*["'`]((?:https?:)?\/\/[^\s"'`]+)["'`]/gi;
3252
+ let fetchMatch;
3253
+ while ((fetchMatch = fetchUrlPattern.exec(stripped)) !== null) {
3254
+ const url = fetchMatch[1];
3255
+ if (/^(?:https?:)?\/\/(?:localhost|127\.0\.0\.1)\b/.test(url)) continue;
3256
+ if (isDomainAllowed(url)) continue;
3257
+ externalUrlCount++;
3258
+ errors.push({
3259
+ rule: 3,
3260
+ severity: "critical",
3261
+ message: `External fetch("${snippet(url, 80)}") found in script. Apps must be self-contained or declare network capability with network_allow.`,
3262
+ match: snippet(fetchMatch[0]),
3263
+ line: lineAt(html, fetchMatch.index)
3264
+ });
3265
+ }
3266
+ const xhrOpenPattern = /\.open\s*\(\s*["'][A-Z]+["']\s*,\s*["']((?:https?:)?\/\/[^\s"']+)["']/gi;
3267
+ let xhrMatch;
3268
+ while ((xhrMatch = xhrOpenPattern.exec(stripped)) !== null) {
3269
+ const url = xhrMatch[1];
3270
+ if (/^(?:https?:)?\/\/(?:localhost|127\.0\.0\.1)\b/.test(url)) continue;
3271
+ if (isDomainAllowed(url)) continue;
3272
+ externalUrlCount++;
3273
+ errors.push({
3274
+ rule: 3,
3275
+ severity: "critical",
3276
+ message: `External XMLHttpRequest to "${snippet(url, 80)}" found. Apps must be self-contained or declare network capability with network_allow.`,
3277
+ match: snippet(xhrMatch[0]),
3278
+ line: lineAt(html, xhrMatch.index)
3279
+ });
3280
+ }
3244
3281
  const detectedCaps = /* @__PURE__ */ new Set();
3245
3282
  for (const { pattern, capability, label } of API_CAPABILITY_MAP) {
3246
3283
  pattern.lastIndex = 0;
@@ -3353,6 +3390,13 @@ function validateClawHtml(html, manifest, options) {
3353
3390
  });
3354
3391
  }
3355
3392
  if (declaredCaps.has("network") && manifest.network_allow) {
3393
+ if (manifest.network_allow.length > RESOURCE_LIMITS.NETWORK_ALLOW_MAX_ENTRIES) {
3394
+ errors.push({
3395
+ rule: 5,
3396
+ severity: "critical",
3397
+ message: `network_allow has ${manifest.network_allow.length} entries, max ${RESOURCE_LIMITS.NETWORK_ALLOW_MAX_ENTRIES}.`
3398
+ });
3399
+ }
3356
3400
  for (const url of manifest.network_allow) {
3357
3401
  const isWildcard = /^\*\.[a-zA-Z0-9][\w\-.]+$/.test(url);
3358
3402
  if (!isWildcard && !/^(wss?|https?):\/\/.+/.test(url)) {
@@ -3385,7 +3429,10 @@ function validateClawHtml(html, manifest, options) {
3385
3429
  // Bare location.* patterns — negative lookbehind excludes window. prefix to avoid double-reporting
3386
3430
  { pattern: /(?<!window\.)location\.href\s*=/g, fix: "use in-app routing (history.pushState, show/hide views)", label: "location.href =" },
3387
3431
  { pattern: /(?<!window\.)location\.assign\s*\(/g, fix: "use in-app routing (history.pushState, show/hide views)", label: "location.assign()" },
3388
- { pattern: /(?<!window\.)location\.replace\s*\(/g, fix: "use in-app routing (history.pushState, show/hide views)", label: "location.replace()" }
3432
+ { pattern: /(?<!window\.)location\.replace\s*\(/g, fix: "use in-app routing (history.pushState, show/hide views)", label: "location.replace()" },
3433
+ // setTimeout/setInterval with string argument — equivalent to eval (blocked by CSP)
3434
+ { pattern: /setTimeout\s*\(\s*["']/g, fix: "pass a function reference to setTimeout, not a string", label: 'setTimeout("string")' },
3435
+ { pattern: /setInterval\s*\(\s*["']/g, fix: "pass a function reference to setInterval, not a string", label: 'setInterval("string")' }
3389
3436
  ];
3390
3437
  for (const { pattern, fix, label } of FORBIDDEN_DIRECT_APIS) {
3391
3438
  pattern.lastIndex = 0;
@@ -3473,6 +3520,110 @@ function validateClawHtml(html, manifest, options) {
3473
3520
  });
3474
3521
  }
3475
3522
  }
3523
+ if (/async\s+function\s+render\s*\(/.test(html)) {
3524
+ const m = html.match(/async\s+function\s+render\s*\(/);
3525
+ const line = m ? html.substring(0, m.index).split("\n").length : void 0;
3526
+ errors.push({
3527
+ rule: 18,
3528
+ severity: "critical",
3529
+ line,
3530
+ message: "render() must be synchronous. The EC Runtime does NOT await it \u2014 async render causes race conditions.",
3531
+ suggestion: "Remove async keyword: `function render() { ... }`. Move async operations outside render.",
3532
+ autoFixable: false
3533
+ });
3534
+ }
3535
+ {
3536
+ const renderMatch = html.match(/function\s+render\s*\([^)]*\)\s*\{/);
3537
+ if (renderMatch && renderMatch.index !== void 0) {
3538
+ let depth = 0;
3539
+ let start = renderMatch.index + renderMatch[0].length - 1;
3540
+ let end = start;
3541
+ for (let i = start; i < html.length; i++) {
3542
+ if (html[i] === "{") depth++;
3543
+ else if (html[i] === "}") {
3544
+ depth--;
3545
+ if (depth === 0) {
3546
+ end = i;
3547
+ break;
3548
+ }
3549
+ }
3550
+ }
3551
+ const body = html.substring(start, end);
3552
+ if (/setInterval\s*\(/.test(body)) {
3553
+ errors.push({
3554
+ rule: 19,
3555
+ severity: "warning",
3556
+ message: "setInterval() inside render() \u2014 intervals accumulate on every re-render. Move outside render or clear previous interval first.",
3557
+ autoFixable: false
3558
+ });
3559
+ }
3560
+ if (/addEventListener\s*\(/.test(body)) {
3561
+ errors.push({
3562
+ rule: 19,
3563
+ severity: "warning",
3564
+ message: "addEventListener() inside render() \u2014 listeners accumulate on every re-render. Move to init() or use named functions.",
3565
+ autoFixable: false
3566
+ });
3567
+ }
3568
+ }
3569
+ }
3570
+ {
3571
+ const lsPattern = /localStorage\.(setItem|getItem|removeItem)\s*\(/g;
3572
+ let lsMatch;
3573
+ while ((lsMatch = lsPattern.exec(html)) !== null) {
3574
+ if (isInScriptBlock(html, lsMatch.index)) {
3575
+ const line = html.substring(0, lsMatch.index).split("\n").length;
3576
+ errors.push({
3577
+ rule: 20,
3578
+ severity: "warning",
3579
+ line,
3580
+ match: lsMatch[0],
3581
+ message: "Direct localStorage is not app-scoped and may collide. Use initState/mutateState or echoclaw.db instead.",
3582
+ autoFixable: false
3583
+ });
3584
+ break;
3585
+ }
3586
+ }
3587
+ }
3588
+ if (/JSON\.parse\(\s*\w+\.body\s*\)/.test(html)) {
3589
+ errors.push({
3590
+ rule: 21,
3591
+ severity: "warning",
3592
+ message: "Bridge fetch response.body is already parsed (object for JSON). Do NOT JSON.parse(res.body) \u2014 use res.body directly.",
3593
+ autoFixable: false
3594
+ });
3595
+ }
3596
+ {
3597
+ const stateOverride = /\b(var|let|const)\s+state\s*=/.exec(html);
3598
+ if (stateOverride && isInScriptBlock(html, stateOverride.index)) {
3599
+ const line = html.substring(0, stateOverride.index).split("\n").length;
3600
+ errors.push({
3601
+ rule: 22,
3602
+ severity: "critical",
3603
+ line,
3604
+ match: stateOverride[0],
3605
+ message: "`state` is a reserved EC Runtime global. Declaring it with var/let/const shadows or conflicts with window.state.",
3606
+ suggestion: "Use a different variable name (e.g., appState, localState).",
3607
+ autoFixable: false
3608
+ });
3609
+ }
3610
+ }
3611
+ {
3612
+ const cssVarPattern = /var\(--(?!ec-)(?:bg-|txt-|agent|border-|radius-|space-)/g;
3613
+ let cssMatch;
3614
+ while ((cssMatch = cssVarPattern.exec(html)) !== null) {
3615
+ if (isInStyleAttribute(html, cssMatch.index) || isInStyleBlock(html, cssMatch.index)) {
3616
+ errors.push({
3617
+ rule: 23,
3618
+ severity: "warning",
3619
+ match: cssMatch[0],
3620
+ message: `CSS variable missing --ec- prefix: ${cssMatch[0]}). Use --ec-bg-page, --ec-txt-primary, --ec-agent, etc.`,
3621
+ autoFixable: false
3622
+ });
3623
+ break;
3624
+ }
3625
+ }
3626
+ }
3476
3627
  const autoFixableCount = errors.filter((e) => e.autoFixable === true).length;
3477
3628
  const filteredErrors = phase === "soft" ? errors.filter((e) => e.autoFixable === true) : errors;
3478
3629
  const hasCritical = filteredErrors.some((e) => e.severity === "critical");
@@ -4221,6 +4372,7 @@ I build and deliver; the client validates and monitors. If violations found, cli
4221
4372
  | \`<!--CLAW_FEEDBACK:{"appId":"APP-<id>","code":"sandbox_violations","message":"..."}-->\` | Fix ONLY the listed violations \u2192 re-write files \u2192 emit CLAW_APP_READY. |
4222
4373
  | \`<!--CLAW_FEEDBACK:{"appId":"APP-<id>","code":"sync_failed","message":"..."}-->\` | Package \`index.html\` + \`claw.json\` + \`session.json\` into a \`.claw\` JSON \u2192 output in \`<details><summary>Click to install</summary>\u2026</details>\`. |
4223
4374
  | \`<!--CLAW_FEEDBACK:{"appId":"APP-<id>","code":"runtime_diagnostic","message":"..."}-->\` | \`message\` has \`csp_violations\`, \`js_errors\`, \`fetch_log\`, \`state_ops\`. Fix root cause \u2192 re-deliver. If platform bug (not my code) \u2192 output \`CLAW_BUG_REPORT\`. |
4375
+ | \`<!--CLAW_FEEDBACK:{"appId":"APP-<id>","code":"runtime_validation","message":"..."}-->\` | **Compiler errors** with line numbers + source snippets. App already installed \u2014 fix blockers \u2192 re-deliver as update. |
4224
4376
  | Neither | Normal chat. Never output markers. |
4225
4377
 
4226
4378
  Normal chat + user describes an app idea \u2192 "Use the Create App button in EchoClaw to get started."
@@ -4248,7 +4400,7 @@ Client responds with \`CLAW_FEEDBACK code="runtime_diagnostic"\`. Wait \u2014 do
4248
4400
  **CLAW_BUG_REPORT** \u2014 Platform bug needing official fix (not app code issues):
4249
4401
  \`<!--CLAW_BUG_REPORT:{"appId":"APP-<id>","issue_type":"client_rendering_bug","severity":"blocker","failure":"what went wrong","suggestion":"proposed fix"}-->\`
4250
4402
  issue_type: client_rendering_bug, capability_not_implemented, api_inconsistency. severity: blocker, degraded, cosmetic.
4251
- Only output CLAW_BUG_REPORT after exhausting my own fixes and confirming the issue is in the platform, not my code. When unsure, keep debugging \u2014 never blame the client prematurely. If confirmed, tell the user they can submit feedback to the EchoClaw team (client shows a confirmation bubble).
4403
+ Before requesting new capabilities, **verify existing APIs can't solve it** (bridge fetch, WebSocket, auth, SQLite, shared_libs, NPM deps). Only output CLAW_BUG_REPORT after exhausting existing capabilities AND confirming the issue is in the platform, not my code. When unsure, keep debugging \u2014 never blame the client prematurely.
4252
4404
 
4253
4405
  Markers are progress signals, NOT stopping points. After CLAW_STEP, continue immediately. Entire build + deliver in ONE response. I decide design \u2014 no options offered, but confirm plan before building (create only).
4254
4406
 
@@ -5635,7 +5787,9 @@ var WorkspaceFileReader = class {
5635
5787
  await this._atomicWrite(path4.join(dir, WORKSPACE_FILES.STATUS), "writing");
5636
5788
  const files = splitPackageToFiles(pkg, status);
5637
5789
  await this._atomicWrite(path4.join(dir, WORKSPACE_FILES.PACKAGE), files.packageJson);
5638
- await this._atomicWrite(path4.join(dir, WORKSPACE_FILES.HTML), files.html);
5790
+ if (files.html) {
5791
+ await this._atomicWrite(path4.join(dir, WORKSPACE_FILES.HTML), files.html);
5792
+ }
5639
5793
  if (files.devlogJson) {
5640
5794
  await this._atomicWrite(path4.join(dir, WORKSPACE_FILES.DEVLOG), files.devlogJson);
5641
5795
  }
@@ -9419,9 +9573,8 @@ var SyncCommandHandler = class {
9419
9573
  if (!appId || typeof appId !== "string") continue;
9420
9574
  desktopAppIds.add(appId);
9421
9575
  try {
9422
- await this._reader.writeAppFiles(appId, pkg, "published");
9423
- } catch (err) {
9424
- console.warn(`[sync] full_sync skip appId=${appId}: ${err.message}`);
9576
+ await this._reader.writeAppStatus(appId, "published");
9577
+ } catch {
9425
9578
  }
9426
9579
  }
9427
9580
  const workspaceAppIds = await this._reader.listApps();
@@ -9501,8 +9654,6 @@ var AgentCore = class {
9501
9654
  _workspaceReader;
9502
9655
  _syncCommandHandler = null;
9503
9656
  _agentVersion;
9504
- /** Called when desktop reports a different version than this agent. */
9505
- onVersionMismatch = null;
9506
9657
  /** Currently attached transport (Phase 1: single transport). */
9507
9658
  _transport = null;
9508
9659
  /** Workspace directory watcher for detecting new apps from OpenClaw. */
@@ -9835,10 +9986,6 @@ var AgentCore = class {
9835
9986
  if (payloadType === "client_hello") {
9836
9987
  const clientVer = payload.version || "unknown";
9837
9988
  console.log(` [client_hello] desktop version=${clientVer}`);
9838
- if (clientVer !== this._agentVersion && clientVer !== "unknown") {
9839
- console.log(` [version] agent=${this._agentVersion} desktop=${clientVer} \u2014 versions differ`);
9840
- this.onVersionMismatch?.(this._agentVersion, clientVer);
9841
- }
9842
9989
  return;
9843
9990
  }
9844
9991
  if (payloadType === "vm_start") {
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  AgentCore,
4
4
  RelayAgent
5
- } from "./chunk-BCFYE35W.js";
5
+ } from "./chunk-HDBJBULU.js";
6
6
  import "./chunk-STI2237I.js";
7
7
 
8
8
  // src/service/platform.ts
@@ -1106,11 +1106,6 @@ async function runDaemon(relay, code, gateway, bridgePort, vmOpts) {
1106
1106
  if (vmConfig) {
1107
1107
  console.log(` [vm] enabled (backend=${vmConfig.backend ?? "docker"} image=${vmConfig.image ?? "default"})`);
1108
1108
  }
1109
- agentCore.onVersionMismatch = (_agentVer, clientVer) => {
1110
- console.log(` [ota] desktop requests version ${clientVer} \u2014 triggering update`);
1111
- updater.updateToVersion(clientVer).catch(() => {
1112
- });
1113
- };
1114
1109
  updater.on("update_available", (info) => {
1115
1110
  console.log(` ${GREEN}[ota]${RESET} update available: ${info.current} \u2192 ${info.latest}`);
1116
1111
  });
@@ -21,8 +21,6 @@ export declare class AgentCore {
21
21
  private readonly _workspaceReader;
22
22
  private _syncCommandHandler;
23
23
  private readonly _agentVersion;
24
- /** Called when desktop reports a different version than this agent. */
25
- onVersionMismatch: ((agentVersion: string, clientVersion: string) => void) | null;
26
24
  /** Currently attached transport (Phase 1: single transport). */
27
25
  private _transport;
28
26
  /** Workspace directory watcher for detecting new apps from OpenClaw. */
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  VmManager,
21
21
  buildAppRequestPrompt,
22
22
  restartOpenClaw
23
- } from "./chunk-BCFYE35W.js";
23
+ } from "./chunk-HDBJBULU.js";
24
24
  import {
25
25
  SessionStore
26
26
  } from "./chunk-STI2237I.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echoclaw-relay-agent",
3
- "version": "0.33.2",
3
+ "version": "0.33.4",
4
4
  "description": "EchoClaw Relay Connection — E2E encrypted relay transport, pairing, and session management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",