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.
- package/bin/lane-watchdog.js +54 -23
- package/bin/mesh-agent.js +49 -18
- package/bin/mesh-bridge.js +3 -2
- package/bin/mesh-deploy.js +4 -0
- package/bin/mesh-health-publisher.js +41 -1
- package/bin/mesh-task-daemon.js +14 -4
- package/bin/mesh.js +17 -43
- package/install.sh +3 -2
- package/lib/agent-activity.js +2 -2
- package/lib/exec-safety.js +163 -0
- package/lib/kanban-io.js +20 -33
- package/lib/llm-providers.js +27 -0
- package/lib/mcp-knowledge/core.mjs +7 -5
- package/lib/mcp-knowledge/server.mjs +8 -1
- package/lib/mesh-collab.js +274 -250
- package/lib/mesh-harness.js +6 -0
- package/lib/mesh-plans.js +84 -45
- package/lib/mesh-tasks.js +113 -81
- package/lib/nats-resolve.js +4 -4
- package/lib/pre-compression-flush.mjs +2 -0
- package/lib/session-store.mjs +6 -3
- package/mission-control/package-lock.json +4188 -3698
- package/mission-control/package.json +2 -2
- package/mission-control/src/app/api/diagnostics/route.ts +8 -0
- package/mission-control/src/app/api/diagnostics/test-runner/route.ts +8 -0
- package/mission-control/src/app/api/memory/graph/route.ts +34 -18
- package/mission-control/src/app/api/memory/search/route.ts +9 -5
- package/mission-control/src/app/api/mesh/identity/route.ts +13 -5
- package/mission-control/src/app/api/mesh/nodes/route.ts +8 -0
- package/mission-control/src/app/api/settings/gateway/route.ts +62 -0
- package/mission-control/src/app/api/souls/[id]/evolution/route.ts +49 -12
- package/mission-control/src/app/api/souls/[id]/prompt/route.ts +7 -1
- package/mission-control/src/app/api/souls/[id]/propagate/route.ts +24 -5
- package/mission-control/src/app/api/souls/route.ts +6 -4
- package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +7 -1
- package/mission-control/src/app/api/tasks/[id]/route.ts +20 -4
- package/mission-control/src/app/api/tasks/route.ts +68 -9
- package/mission-control/src/app/api/workspace/read/route.ts +11 -0
- package/mission-control/src/lib/config.ts +11 -2
- package/mission-control/src/lib/db/index.ts +16 -1
- package/mission-control/src/lib/memory/extract.ts +2 -1
- package/mission-control/src/lib/memory/retrieval.ts +3 -2
- package/mission-control/src/lib/sync/tasks.ts +4 -1
- package/mission-control/src/middleware.ts +82 -0
- package/package.json +1 -1
- package/services/launchd/ai.openclaw.lane-watchdog.plist +1 -1
- package/services/launchd/ai.openclaw.log-rotate.plist +11 -0
- package/services/launchd/ai.openclaw.mesh-agent.plist +4 -0
- package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +4 -0
- package/services/launchd/ai.openclaw.mesh-health-publisher.plist +4 -0
- package/services/launchd/ai.openclaw.mission-control.plist +5 -4
- 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:
|
|
128
|
-
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
|
-
|
|
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
|
|
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 || "
|
|
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
|
-
|
|
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.
|
|
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
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<key>HOME</key>
|
|
27
27
|
<string>${HOME}</string>
|
|
28
28
|
<key>PATH</key>
|
|
29
|
-
<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>
|
|
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
|
|
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>
|
|
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
|
|
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 "
|
|
62
|
+
info "Legacy mesh agent service removed"
|
|
48
63
|
fi
|
|
49
64
|
elif [ "$OS" = "Darwin" ]; then
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|