openclaw-node-harness 2.1.0 → 2.2.0

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 (52) hide show
  1. package/bin/lane-watchdog.js +54 -23
  2. package/bin/mesh-agent.js +49 -18
  3. package/bin/mesh-bridge.js +3 -2
  4. package/bin/mesh-deploy.js +4 -0
  5. package/bin/mesh-health-publisher.js +41 -1
  6. package/bin/mesh-task-daemon.js +14 -4
  7. package/bin/mesh.js +17 -43
  8. package/install.sh +3 -2
  9. package/lib/agent-activity.js +2 -2
  10. package/lib/exec-safety.js +163 -0
  11. package/lib/kanban-io.js +20 -33
  12. package/lib/llm-providers.js +27 -0
  13. package/lib/mcp-knowledge/core.mjs +7 -5
  14. package/lib/mcp-knowledge/server.mjs +8 -1
  15. package/lib/mesh-collab.js +274 -250
  16. package/lib/mesh-harness.js +6 -0
  17. package/lib/mesh-plans.js +84 -45
  18. package/lib/mesh-tasks.js +113 -81
  19. package/lib/nats-resolve.js +4 -4
  20. package/lib/pre-compression-flush.mjs +2 -0
  21. package/lib/session-store.mjs +6 -3
  22. package/mission-control/package-lock.json +4188 -3698
  23. package/mission-control/package.json +2 -2
  24. package/mission-control/src/app/api/diagnostics/route.ts +8 -0
  25. package/mission-control/src/app/api/diagnostics/test-runner/route.ts +8 -0
  26. package/mission-control/src/app/api/memory/graph/route.ts +34 -18
  27. package/mission-control/src/app/api/memory/search/route.ts +9 -5
  28. package/mission-control/src/app/api/mesh/identity/route.ts +13 -5
  29. package/mission-control/src/app/api/mesh/nodes/route.ts +8 -0
  30. package/mission-control/src/app/api/settings/gateway/route.ts +62 -0
  31. package/mission-control/src/app/api/souls/[id]/evolution/route.ts +49 -12
  32. package/mission-control/src/app/api/souls/[id]/prompt/route.ts +7 -1
  33. package/mission-control/src/app/api/souls/[id]/propagate/route.ts +24 -5
  34. package/mission-control/src/app/api/souls/route.ts +6 -4
  35. package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +7 -1
  36. package/mission-control/src/app/api/tasks/[id]/route.ts +20 -4
  37. package/mission-control/src/app/api/tasks/route.ts +68 -9
  38. package/mission-control/src/app/api/workspace/read/route.ts +11 -0
  39. package/mission-control/src/lib/config.ts +11 -2
  40. package/mission-control/src/lib/db/index.ts +16 -1
  41. package/mission-control/src/lib/memory/extract.ts +2 -1
  42. package/mission-control/src/lib/memory/retrieval.ts +3 -2
  43. package/mission-control/src/lib/sync/tasks.ts +4 -1
  44. package/mission-control/src/middleware.ts +82 -0
  45. package/package.json +1 -1
  46. package/services/launchd/ai.openclaw.lane-watchdog.plist +1 -1
  47. package/services/launchd/ai.openclaw.log-rotate.plist +11 -0
  48. package/services/launchd/ai.openclaw.mesh-agent.plist +4 -0
  49. package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +4 -0
  50. package/services/launchd/ai.openclaw.mesh-health-publisher.plist +4 -0
  51. package/services/launchd/ai.openclaw.mission-control.plist +5 -4
  52. package/uninstall.sh +37 -9
@@ -12,6 +12,12 @@ import { getNats, sc } from "@/lib/nats";
12
12
  import { WORKSPACE_ROOT, AGENT_NAME } from "@/lib/config";
13
13
  import path from "path";
14
14
 
15
+ /** Safely parse a JSON string from a DB field, returning fallback on failure. */
16
+ function safeParse(json: string | null, fallback: unknown = []): unknown {
17
+ if (!json) return fallback;
18
+ try { return JSON.parse(json); } catch { return fallback; }
19
+ }
20
+
15
21
  /**
16
22
  * Parse .companion-state.md to extract current active task.
17
23
  * Returns a synthetic task object if there's active work, null otherwise.
@@ -121,11 +127,11 @@ export async function GET(request: NextRequest) {
121
127
  .all();
122
128
  }
123
129
 
124
- // Parse JSON fields for the response
130
+ // Parse JSON fields for the response (per-row guard against corrupt data)
125
131
  const result = rows.map((t) => ({
126
132
  ...t,
127
- successCriteria: t.successCriteria ? JSON.parse(t.successCriteria) : [],
128
- artifacts: t.artifacts ? JSON.parse(t.artifacts) : [],
133
+ successCriteria: safeParse(t.successCriteria),
134
+ artifacts: safeParse(t.artifacts),
129
135
  }));
130
136
 
131
137
  return NextResponse.json(result);
@@ -155,6 +161,60 @@ export async function POST(request: NextRequest) {
155
161
  );
156
162
  }
157
163
 
164
+ if (body.title.length > 500) {
165
+ return NextResponse.json(
166
+ { error: "title must be 500 characters or fewer" },
167
+ { status: 400 }
168
+ );
169
+ }
170
+
171
+ if (body.success_criteria !== undefined) {
172
+ if (!Array.isArray(body.success_criteria)) {
173
+ if (typeof body.success_criteria === "string") {
174
+ try {
175
+ const parsed = JSON.parse(body.success_criteria);
176
+ if (!Array.isArray(parsed)) {
177
+ return NextResponse.json(
178
+ { error: "success_criteria must be a JSON array" },
179
+ { status: 400 }
180
+ );
181
+ }
182
+ } catch {
183
+ return NextResponse.json(
184
+ { error: "success_criteria must be a valid JSON array string" },
185
+ { status: 400 }
186
+ );
187
+ }
188
+ } else {
189
+ return NextResponse.json(
190
+ { error: "success_criteria must be an array or JSON array string" },
191
+ { status: 400 }
192
+ );
193
+ }
194
+ }
195
+ }
196
+
197
+ const VALID_STATUSES = [
198
+ "not started", "queued", "ready", "submitted", "running",
199
+ "blocked", "waiting-user", "done", "cancelled", "archived",
200
+ ];
201
+ if (body.status && !VALID_STATUSES.includes(body.status)) {
202
+ return NextResponse.json(
203
+ { error: `status must be one of: ${VALID_STATUSES.join(", ")}` },
204
+ { status: 400 }
205
+ );
206
+ }
207
+
208
+ if (body.scheduled_date) {
209
+ const d = new Date(body.scheduled_date);
210
+ if (isNaN(d.getTime())) {
211
+ return NextResponse.json(
212
+ { error: "scheduled_date must be a valid ISO date string" },
213
+ { status: 400 }
214
+ );
215
+ }
216
+ }
217
+
158
218
  const status = body.status || "not started";
159
219
  const now = new Date();
160
220
  // Allow custom IDs for projects/pipelines/phases; auto-generate for tasks
@@ -227,18 +287,17 @@ export async function POST(request: NextRequest) {
227
287
  return NextResponse.json(
228
288
  {
229
289
  ...created,
230
- successCriteria: created?.successCriteria
231
- ? JSON.parse(created.successCriteria)
232
- : [],
233
- artifacts: created?.artifacts ? JSON.parse(created.artifacts) : [],
290
+ successCriteria: safeParse(created?.successCriteria ?? null),
291
+ artifacts: safeParse(created?.artifacts ?? null),
234
292
  },
235
293
  { status: 201 }
236
294
  );
237
295
  } catch (err) {
238
296
  console.error("POST /api/tasks error:", err);
297
+ const status = err instanceof SyntaxError ? 400 : 500;
239
298
  return NextResponse.json(
240
- { error: "Failed to create task" },
241
- { status: 500 }
299
+ { error: status === 400 ? String(err) : "Failed to create task" },
300
+ { status }
242
301
  );
243
302
  }
244
303
  }
@@ -25,6 +25,17 @@ export async function GET(request: NextRequest) {
25
25
  return NextResponse.json({ error: "File not found" }, { status: 404 });
26
26
  }
27
27
 
28
+ // Defeat symlink traversal: resolve the real path and re-check prefix
29
+ const realPath = fs.realpathSync(absPath);
30
+ const realRoot = fs.realpathSync(WORKSPACE_ROOT);
31
+ if (!realPath.startsWith(realRoot + path.sep) && realPath !== realRoot) {
32
+ return NextResponse.json({ error: "Path traversal denied" }, { status: 403 });
33
+ }
34
+
35
+ if (!fs.existsSync(realPath)) {
36
+ return NextResponse.json({ error: "File not found" }, { status: 404 });
37
+ }
38
+
28
39
  const stat = fs.statSync(absPath);
29
40
  if (stat.isDirectory()) {
30
41
  return NextResponse.json({ error: "Path is a directory" }, { status: 400 });
@@ -1,7 +1,8 @@
1
1
  import path from "path";
2
+ import { hostname, homedir } from "os";
2
3
 
3
4
  export const WORKSPACE_ROOT =
4
- process.env.WORKSPACE_ROOT || "/Users/moltymac/.openclaw/workspace";
5
+ process.env.WORKSPACE_ROOT || path.join(homedir(), ".openclaw", "workspace");
5
6
 
6
7
  export const DB_PATH =
7
8
  process.env.DB_PATH ||
@@ -65,8 +66,16 @@ export function getProviderModels(provider?: string): Record<CapabilityTier, str
65
66
  /** All registered provider names */
66
67
  export const AVAILABLE_PROVIDERS = Object.keys(MODEL_MAP);
67
68
 
69
+ /** Validates that a URL path parameter is safe for use in file paths. */
70
+ export function validatePathParam(param: string): string {
71
+ const cleaned = param.trim();
72
+ if (!cleaned || !/^[\w][\w.-]{0,127}$/.test(cleaned)) {
73
+ throw new Error(`Invalid path parameter: "${param.slice(0, 40)}"`);
74
+ }
75
+ return cleaned;
76
+ }
77
+
68
78
  // ── Node Identity (Distributed MC) ──
69
- import { hostname } from "os";
70
79
 
71
80
  /** This node's unique identifier in the mesh */
72
81
  export const NODE_ID = process.env.OPENCLAW_NODE_ID || hostname();
@@ -2,7 +2,7 @@ import Database from "better-sqlite3";
2
2
  import { drizzle } from "drizzle-orm/better-sqlite3";
3
3
  import * as schema from "./schema";
4
4
  import { DB_PATH } from "../config";
5
- import fs from "fs";
5
+ import fs, { chmodSync, existsSync } from "fs";
6
6
  import path from "path";
7
7
 
8
8
  let _db: ReturnType<typeof drizzle<typeof schema>> | null = null;
@@ -488,6 +488,21 @@ export function getDb() {
488
488
 
489
489
  runMigrations(_sqlite);
490
490
 
491
+ // Lock down DB file permissions — owner read/write only
492
+ try {
493
+ if (existsSync(DB_PATH)) {
494
+ chmodSync(DB_PATH, 0o600);
495
+ }
496
+ const walPath = DB_PATH + "-wal";
497
+ if (existsSync(walPath)) {
498
+ chmodSync(walPath, 0o600);
499
+ }
500
+ const journalPath = DB_PATH + "-journal";
501
+ if (existsSync(journalPath)) {
502
+ chmodSync(journalPath, 0o600);
503
+ }
504
+ } catch {}
505
+
491
506
  _db = drizzle(_sqlite, { schema });
492
507
  return _db;
493
508
  }
@@ -263,7 +263,8 @@ export function getItemsWithSource(limit = 50, offset = 0) {
263
263
  */
264
264
  export function searchItems(query: string, category?: string, limit = 20) {
265
265
  const raw = getRawDb();
266
- const safeQuery = query.replace(/"/g, '""');
266
+ const safeQuery = query.replace(/"/g, '""').replace(/[*(){}^]/g, '').trim();
267
+ if (!safeQuery) return [];
267
268
 
268
269
  if (category) {
269
270
  return raw
@@ -35,8 +35,9 @@ function decay(relevance: number, daysOld: number, rate = 0.01): number {
35
35
  * Multi-word → ("word1 word2") OR ("word1"* OR "word2"*)
36
36
  */
37
37
  function buildFtsQuery(query: string): string {
38
- const safe = query.replace(/"/g, '""');
39
- const terms = safe.trim().split(/\s+/).filter((t) => t.length >= 2);
38
+ const safe = query.replace(/"/g, '""').replace(/[*(){}^]/g, '').trim();
39
+ if (!safe) return '""';
40
+ const terms = safe.split(/\s+/).filter((t) => t.length >= 2);
40
41
  if (terms.length <= 1) return `"${safe}"*`;
41
42
  const phrase = `"${safe}"`;
42
43
  const individual = terms.map((t) => `"${t}"*`).join(" OR ");
@@ -283,7 +283,10 @@ export function syncTasksToMarkdown(db: DrizzleDb): void {
283
283
  if (!fs.existsSync(dir)) {
284
284
  fs.mkdirSync(dir, { recursive: true });
285
285
  }
286
- fs.writeFileSync(tmpPath, markdown, "utf-8");
286
+ const fd = fs.openSync(tmpPath, "w");
287
+ fs.writeSync(fd, markdown, 0, "utf-8");
288
+ fs.fsyncSync(fd);
289
+ fs.closeSync(fd);
287
290
  fs.renameSync(tmpPath, ACTIVE_TASKS_MD);
288
291
 
289
292
  // Record mtime of our own write so we don't re-import it
@@ -0,0 +1,82 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ /**
4
+ * API authentication middleware.
5
+ *
6
+ * Protects all /api/* routes with a Bearer token check.
7
+ * Token is read from MC_AUTH_TOKEN env var. If unset, auth is disabled
8
+ * (localhost-only deployments). When set, every API request must include:
9
+ * Authorization: Bearer <token>
10
+ *
11
+ * Page routes (non-API) are not gated — the dashboard is a local UI.
12
+ */
13
+
14
+ const AUTH_TOKEN = process.env.MC_AUTH_TOKEN || "";
15
+
16
+ export function middleware(request: NextRequest) {
17
+ // Only gate API routes
18
+ if (!request.nextUrl.pathname.startsWith("/api/")) {
19
+ return NextResponse.next();
20
+ }
21
+
22
+ // Body size limit (1MB) for mutation requests
23
+ const contentLength = parseInt(request.headers.get("content-length") || "0", 10);
24
+ const MAX_BODY_SIZE = 1024 * 1024; // 1MB
25
+ if (contentLength > MAX_BODY_SIZE) {
26
+ return NextResponse.json(
27
+ { error: `Request body too large (${contentLength} bytes, max ${MAX_BODY_SIZE})` },
28
+ { status: 413 }
29
+ );
30
+ }
31
+
32
+ // SSE endpoints use EventSource which can't set headers — allow if
33
+ // the token is passed as a query param instead.
34
+ const tokenFromQuery = request.nextUrl.searchParams.get("token");
35
+
36
+ // If no token is configured, auth is disabled (backwards-compatible)
37
+ if (!AUTH_TOKEN) {
38
+ return NextResponse.next();
39
+ }
40
+
41
+ const authHeader = request.headers.get("authorization") || "";
42
+ const bearer = authHeader.startsWith("Bearer ")
43
+ ? authHeader.slice(7).trim()
44
+ : "";
45
+
46
+ const providedToken = bearer || tokenFromQuery || "";
47
+
48
+ if (!providedToken) {
49
+ return NextResponse.json(
50
+ { error: "Missing Authorization header" },
51
+ { status: 401 }
52
+ );
53
+ }
54
+
55
+ // Constant-time comparison to prevent timing attacks
56
+ if (!timingSafeEqual(providedToken, AUTH_TOKEN)) {
57
+ return NextResponse.json({ error: "Invalid token" }, { status: 403 });
58
+ }
59
+
60
+ return NextResponse.next();
61
+ }
62
+
63
+ /** Constant-time string comparison (Edge Runtime compatible). */
64
+ function timingSafeEqual(a: string, b: string): boolean {
65
+ if (a.length !== b.length) {
66
+ // Still do a full comparison to avoid length-based timing leak
67
+ let result = a.length ^ b.length;
68
+ for (let i = 0; i < a.length; i++) {
69
+ result |= a.charCodeAt(i) ^ (b.charCodeAt(i % b.length) || 0);
70
+ }
71
+ return result === 0;
72
+ }
73
+ let result = 0;
74
+ for (let i = 0; i < a.length; i++) {
75
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
76
+ }
77
+ return result === 0;
78
+ }
79
+
80
+ export const config = {
81
+ matcher: "/api/:path*",
82
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-node-harness",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "One-command installer for the OpenClaw node layer — identity, skills, souls, daemon, and Mission Control.",
5
5
  "bin": {
6
6
  "openclaw-node": "./cli.js"
@@ -26,7 +26,7 @@
26
26
  <key>HOME</key>
27
27
  <string>${HOME}</string>
28
28
  <key>PATH</key>
29
- <string>/usr/local/bin:/usr/bin:/bin</string>
29
+ <string>${HOME}/.bun/bin:${HOME}/.local/bin:${HOME}/.npm-global/bin:${HOME}/bin:${HOME}/.volta/bin:${HOME}/.asdf/shims:${HOME}/Library/Application Support/fnm/aliases/default/bin:${HOME}/.fnm/aliases/default/bin:${HOME}/Library/pnpm:${HOME}/.local/share/pnpm:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
30
30
  </dict>
31
31
  </dict>
32
32
  </plist>
@@ -20,6 +20,17 @@
20
20
  <key>Minute</key>
21
21
  <integer>0</integer>
22
22
  </dict>
23
+ <key>RunAtLoad</key>
24
+ <false/>
25
+ <key>KeepAlive</key>
26
+ <false/>
27
+ <key>EnvironmentVariables</key>
28
+ <dict>
29
+ <key>HOME</key>
30
+ <string>${HOME}</string>
31
+ <key>PATH</key>
32
+ <string>${HOME}/.bun/bin:${HOME}/.local/bin:${HOME}/.npm-global/bin:${HOME}/bin:${HOME}/.volta/bin:${HOME}/.asdf/shims:${HOME}/Library/Application Support/fnm/aliases/default/bin:${HOME}/.fnm/aliases/default/bin:${HOME}/Library/pnpm:${HOME}/.local/share/pnpm:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
33
+ </dict>
23
34
  <key>StandardOutPath</key>
24
35
  <string>${HOME}/.openclaw/logs/log-rotate.log</string>
25
36
  <key>StandardErrorPath</key>
@@ -31,6 +31,10 @@
31
31
  <string>${OPENCLAW_NATS}</string>
32
32
  <key>MESH_WORKSPACE</key>
33
33
  <string>${OPENCLAW_WORKSPACE}</string>
34
+ <key>OPENCLAW_NODE_ID</key>
35
+ <string>${OPENCLAW_NODE_ID}</string>
36
+ <key>OPENCLAW_NODE_ROLE</key>
37
+ <string>${OPENCLAW_NODE_ROLE}</string>
34
38
  </dict>
35
39
  <key>ThrottleInterval</key>
36
40
  <integer>30</integer>
@@ -12,6 +12,10 @@
12
12
  </array>
13
13
  <key>EnvironmentVariables</key>
14
14
  <dict>
15
+ <key>HOME</key>
16
+ <string>${HOME}</string>
17
+ <key>PATH</key>
18
+ <string>${HOME}/.bun/bin:${HOME}/.local/bin:${HOME}/.npm-global/bin:${HOME}/bin:${HOME}/.volta/bin:${HOME}/.asdf/shims:${HOME}/Library/Application Support/fnm/aliases/default/bin:${HOME}/.fnm/aliases/default/bin:${HOME}/Library/pnpm:${HOME}/.local/share/pnpm:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
15
19
  <key>OPENCLAW_NODE_ID</key>
16
20
  <string>${OPENCLAW_NODE_ID}</string>
17
21
  <key>OPENCLAW_NATS</key>
@@ -12,6 +12,10 @@
12
12
  </array>
13
13
  <key>EnvironmentVariables</key>
14
14
  <dict>
15
+ <key>HOME</key>
16
+ <string>${HOME}</string>
17
+ <key>PATH</key>
18
+ <string>${HOME}/.bun/bin:${HOME}/.local/bin:${HOME}/.npm-global/bin:${HOME}/bin:${HOME}/.volta/bin:${HOME}/.asdf/shims:${HOME}/Library/Application Support/fnm/aliases/default/bin:${HOME}/.fnm/aliases/default/bin:${HOME}/Library/pnpm:${HOME}/.local/share/pnpm:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
15
19
  <key>OPENCLAW_NODE_ID</key>
16
20
  <string>${OPENCLAW_NODE_ID}</string>
17
21
  <key>OPENCLAW_NATS</key>
@@ -11,16 +11,17 @@
11
11
  <key>ProgramArguments</key>
12
12
  <array>
13
13
  <string>${HOME}/.openclaw/bin/npm</string>
14
- <string>run</string>
15
- <string>dev</string>
14
+ <string>start</string>
16
15
  </array>
17
16
 
18
17
  <key>EnvironmentVariables</key>
19
18
  <dict>
19
+ <key>HOME</key>
20
+ <string>${HOME}</string>
20
21
  <key>PATH</key>
21
- <string>/usr/local/bin:/usr/bin:/bin</string>
22
+ <string>${HOME}/.bun/bin:${HOME}/.local/bin:${HOME}/.npm-global/bin:${HOME}/bin:${HOME}/.volta/bin:${HOME}/.asdf/shims:${HOME}/Library/Application Support/fnm/aliases/default/bin:${HOME}/.fnm/aliases/default/bin:${HOME}/Library/pnpm:${HOME}/.local/share/pnpm:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
22
23
  <key>NODE_ENV</key>
23
- <string>development</string>
24
+ <string>production</string>
24
25
  </dict>
25
26
 
26
27
  <key>RunAtLoad</key>
package/uninstall.sh CHANGED
@@ -35,24 +35,52 @@ if [ -f "$WORKSPACE/bin/install-daemon" ]; then
35
35
  bash "$WORKSPACE/bin/install-daemon" --uninstall 2>/dev/null || true
36
36
  fi
37
37
 
38
- # Stop and remove mesh agent service (if installed)
38
+ # Stop and remove services
39
39
  OS="$(uname -s)"
40
40
  if [ "$OS" = "Linux" ]; then
41
+ # --- Current services: openclaw-*.service under user systemd ---
42
+ SYSTEMD_USER_DIR="$HOME/.config/systemd/user"
43
+ if [ -d "$SYSTEMD_USER_DIR" ]; then
44
+ for unit in "$SYSTEMD_USER_DIR"/openclaw-*.service "$SYSTEMD_USER_DIR"/openclaw-*.timer; do
45
+ [ -f "$unit" ] || continue
46
+ UNIT_NAME="$(basename "$unit")"
47
+ info "Stopping $UNIT_NAME..."
48
+ systemctl --user stop "$UNIT_NAME" 2>/dev/null || true
49
+ systemctl --user disable "$UNIT_NAME" 2>/dev/null || true
50
+ rm -f "$unit"
51
+ info "Removed $UNIT_NAME"
52
+ done
53
+ systemctl --user daemon-reload 2>/dev/null || true
54
+ fi
55
+ # --- Legacy fallback: old system-level openclaw-agent ---
41
56
  if systemctl is-active --quiet openclaw-agent 2>/dev/null; then
42
- info "Stopping mesh agent..."
57
+ info "Stopping legacy mesh agent (openclaw-agent)..."
43
58
  sudo systemctl stop openclaw-agent 2>/dev/null || true
44
59
  sudo systemctl disable openclaw-agent 2>/dev/null || true
45
60
  sudo rm -f /etc/systemd/system/openclaw-agent.service
46
61
  sudo systemctl daemon-reload 2>/dev/null || true
47
- info "Mesh agent service removed"
62
+ info "Legacy mesh agent service removed"
48
63
  fi
49
64
  elif [ "$OS" = "Darwin" ]; then
50
- MESH_PLIST="/Library/LaunchDaemons/com.openclaw.agent.plist"
51
- if [ -f "$MESH_PLIST" ]; then
52
- info "Stopping mesh agent..."
53
- sudo launchctl unload "$MESH_PLIST" 2>/dev/null || true
54
- sudo rm -f "$MESH_PLIST"
55
- info "Mesh agent LaunchDaemon removed"
65
+ # --- Current services: ai.openclaw.*.plist under ~/Library/LaunchAgents ---
66
+ LAUNCHD_AGENTS_DIR="$HOME/Library/LaunchAgents"
67
+ if [ -d "$LAUNCHD_AGENTS_DIR" ]; then
68
+ for plist in "$LAUNCHD_AGENTS_DIR"/ai.openclaw.*.plist; do
69
+ [ -f "$plist" ] || continue
70
+ PLIST_NAME="$(basename "$plist")"
71
+ info "Unloading $PLIST_NAME..."
72
+ launchctl unload "$plist" 2>/dev/null || true
73
+ rm -f "$plist"
74
+ info "Removed $PLIST_NAME"
75
+ done
76
+ fi
77
+ # --- Legacy fallback: old system-level com.openclaw.agent ---
78
+ LEGACY_PLIST="/Library/LaunchDaemons/com.openclaw.agent.plist"
79
+ if [ -f "$LEGACY_PLIST" ]; then
80
+ info "Stopping legacy mesh agent (com.openclaw.agent)..."
81
+ sudo launchctl unload "$LEGACY_PLIST" 2>/dev/null || true
82
+ sudo rm -f "$LEGACY_PLIST"
83
+ info "Legacy mesh agent LaunchDaemon removed"
56
84
  fi
57
85
  fi
58
86
  # Remove mesh symlinks