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
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"gray-matter": "^4.0.3",
|
|
27
27
|
"lucide-react": "^0.575.0",
|
|
28
28
|
"nats": "^2.29.3",
|
|
29
|
-
"next": "16.1
|
|
29
|
+
"next": "^16.2.1",
|
|
30
30
|
"react": "19.2.3",
|
|
31
31
|
"react-dom": "19.2.3",
|
|
32
32
|
"react-force-graph-2d": "^1.29.1",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@types/node": "^20",
|
|
44
44
|
"@types/react": "^19",
|
|
45
45
|
"@types/react-dom": "^19",
|
|
46
|
-
"drizzle-kit": "^0.31.
|
|
46
|
+
"drizzle-kit": "^0.31.10",
|
|
47
47
|
"eslint": "^9",
|
|
48
48
|
"eslint-config-next": "16.1.6",
|
|
49
49
|
"tailwindcss": "^4.2.1",
|
|
@@ -8,6 +8,7 @@ import { parseTasksMarkdown, serializeTasksMarkdown } from "@/lib/parsers/task-m
|
|
|
8
8
|
export const dynamic = "force-dynamic";
|
|
9
9
|
|
|
10
10
|
export async function GET() {
|
|
11
|
+
try {
|
|
11
12
|
const raw = getRawDb();
|
|
12
13
|
|
|
13
14
|
// Task stats
|
|
@@ -94,4 +95,11 @@ export async function GET() {
|
|
|
94
95
|
nats: natsStatus,
|
|
95
96
|
workspace: workspaceExists,
|
|
96
97
|
});
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error("[diagnostics] error:", err);
|
|
100
|
+
return NextResponse.json(
|
|
101
|
+
{ error: err instanceof Error ? err.message : "Internal server error" },
|
|
102
|
+
{ status: err instanceof SyntaxError ? 400 : 500 }
|
|
103
|
+
);
|
|
104
|
+
}
|
|
97
105
|
}
|
|
@@ -41,6 +41,7 @@ async function runTest(suite: string, name: string, fn: TestFn): Promise<TestRes
|
|
|
41
41
|
* All test tasks use the prefix __TEST__ so they can be safely cleaned up.
|
|
42
42
|
*/
|
|
43
43
|
export async function POST() {
|
|
44
|
+
try {
|
|
44
45
|
const results: TestResult[] = [];
|
|
45
46
|
const db = getDb();
|
|
46
47
|
const raw = getRawDb();
|
|
@@ -987,4 +988,11 @@ export async function POST() {
|
|
|
987
988
|
results,
|
|
988
989
|
timestamp: new Date().toISOString(),
|
|
989
990
|
});
|
|
991
|
+
} catch (err) {
|
|
992
|
+
console.error("[diagnostics/test-runner] error:", err);
|
|
993
|
+
return NextResponse.json(
|
|
994
|
+
{ error: err instanceof Error ? err.message : "Internal server error" },
|
|
995
|
+
{ status: err instanceof SyntaxError ? 400 : 500 }
|
|
996
|
+
);
|
|
997
|
+
}
|
|
990
998
|
}
|
|
@@ -18,31 +18,47 @@ import {
|
|
|
18
18
|
import { getRawDb } from "@/lib/db";
|
|
19
19
|
|
|
20
20
|
export async function GET(request: Request) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(request.url);
|
|
23
|
+
const boot = url.searchParams.get("boot");
|
|
24
|
+
const format = url.searchParams.get("format");
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
if (boot === "true") {
|
|
27
|
+
const block = formatEntityContextBlock();
|
|
28
|
+
return NextResponse.json({ block });
|
|
29
|
+
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
if (format === "viz") {
|
|
32
|
+
return getVizData();
|
|
33
|
+
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
const stats = getGraphStats();
|
|
36
|
+
const topEntities = getTopEntities(10).map((entity) => {
|
|
37
|
+
const relations = getEntityRelations(entity.id, 3);
|
|
38
|
+
return { ...entity, relations };
|
|
39
|
+
});
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
return NextResponse.json({ stats, topEntities });
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error("[memory/graph] GET error:", err);
|
|
44
|
+
return NextResponse.json(
|
|
45
|
+
{ error: err instanceof Error ? err.message : "Internal server error" },
|
|
46
|
+
{ status: err instanceof SyntaxError ? 400 : 500 }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
export async function POST() {
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
try {
|
|
53
|
+
const seeded = seedKnownEntities();
|
|
54
|
+
return NextResponse.json({ seeded, message: `${seeded} entities seeded` });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error("[memory/graph] POST error:", err);
|
|
57
|
+
return NextResponse.json(
|
|
58
|
+
{ error: err instanceof Error ? err.message : "Internal server error" },
|
|
59
|
+
{ status: err instanceof SyntaxError ? 400 : 500 }
|
|
60
|
+
);
|
|
61
|
+
}
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
/**
|
|
@@ -29,14 +29,17 @@ function applyScoreDecay(
|
|
|
29
29
|
* Daedalus does deeper semantic rewriting inline during his own searches.
|
|
30
30
|
*/
|
|
31
31
|
function expandQuery(query: string): string {
|
|
32
|
+
// Escape double quotes and strip FTS5 operators to prevent query injection
|
|
33
|
+
const safeQuery = query.replace(/"/g, '""').replace(/[*(){}^]/g, '').trim();
|
|
34
|
+
if (!safeQuery) return '""';
|
|
32
35
|
// Split into terms and add OR variants for common patterns
|
|
33
|
-
const terms =
|
|
36
|
+
const terms = safeQuery.split(/\s+/);
|
|
34
37
|
if (terms.length === 1) {
|
|
35
38
|
// Single term: use prefix matching
|
|
36
|
-
return `"${
|
|
39
|
+
return `"${safeQuery}"*`;
|
|
37
40
|
}
|
|
38
41
|
// Multi-term: quote as phrase + add individual terms with OR for broader recall
|
|
39
|
-
const phrase = `"${
|
|
42
|
+
const phrase = `"${safeQuery}"`;
|
|
40
43
|
const individual = terms.map((t) => `"${t.replace(/"/g, '""')}"*`).join(" OR ");
|
|
41
44
|
return `(${phrase}) OR (${individual})`;
|
|
42
45
|
}
|
|
@@ -54,7 +57,7 @@ export async function GET(request: NextRequest) {
|
|
|
54
57
|
const source = searchParams.get("source");
|
|
55
58
|
const category = searchParams.get("category");
|
|
56
59
|
const limit = Math.min(parseInt(searchParams.get("limit") || "20", 10), 100);
|
|
57
|
-
const offset = parseInt(searchParams.get("offset") || "0", 10);
|
|
60
|
+
const offset = Math.max(0, Math.min(parseInt(searchParams.get("offset") || "0", 10) || 0, 10000));
|
|
58
61
|
const useDecay = searchParams.get("decay") !== "false"; // default: true
|
|
59
62
|
const decayRate = parseFloat(searchParams.get("decay_rate") || "0.01");
|
|
60
63
|
|
|
@@ -138,6 +141,7 @@ export async function GET(request: NextRequest) {
|
|
|
138
141
|
console.error("GET /api/memory/search error:", err);
|
|
139
142
|
const message =
|
|
140
143
|
err instanceof Error ? err.message : "Failed to search memory";
|
|
141
|
-
|
|
144
|
+
const status = err instanceof SyntaxError || err instanceof RangeError ? 400 : 500;
|
|
145
|
+
return NextResponse.json({ error: message }, { status });
|
|
142
146
|
}
|
|
143
147
|
}
|
|
@@ -3,9 +3,17 @@ import { NODE_ID, NODE_ROLE, NODE_PLATFORM } from "@/lib/config";
|
|
|
3
3
|
export const dynamic = "force-dynamic";
|
|
4
4
|
|
|
5
5
|
export async function GET() {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
try {
|
|
7
|
+
return Response.json({
|
|
8
|
+
nodeId: NODE_ID,
|
|
9
|
+
role: NODE_ROLE,
|
|
10
|
+
platform: NODE_PLATFORM,
|
|
11
|
+
});
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error("[mesh/identity] error:", err);
|
|
14
|
+
return Response.json(
|
|
15
|
+
{ error: err instanceof Error ? err.message : "Internal server error" },
|
|
16
|
+
{ status: 500 }
|
|
17
|
+
);
|
|
18
|
+
}
|
|
11
19
|
}
|
|
@@ -85,6 +85,7 @@ const KNOWN_NODES = [
|
|
|
85
85
|
* No synchronous round-trips to nodes. No timeout races. No flickering.
|
|
86
86
|
*/
|
|
87
87
|
export async function GET() {
|
|
88
|
+
try {
|
|
88
89
|
const kv = await getHealthKv();
|
|
89
90
|
const db = getDb();
|
|
90
91
|
const now = Date.now();
|
|
@@ -218,4 +219,11 @@ export async function GET() {
|
|
|
218
219
|
}
|
|
219
220
|
|
|
220
221
|
return NextResponse.json({ nodes, tokenStats });
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error("[mesh/nodes] error:", err);
|
|
224
|
+
return NextResponse.json(
|
|
225
|
+
{ error: err instanceof Error ? err.message : "Internal server error" },
|
|
226
|
+
{ status: err instanceof SyntaxError ? 400 : 500 }
|
|
227
|
+
);
|
|
228
|
+
}
|
|
221
229
|
}
|
|
@@ -52,9 +52,63 @@ export async function GET() {
|
|
|
52
52
|
* Update gateway-related settings. Accepts partial updates.
|
|
53
53
|
* Body: { heartbeat?: { target, every? }, maxConcurrent?: number }
|
|
54
54
|
*/
|
|
55
|
+
const ALLOWED_TOP_LEVEL_KEYS = new Set([
|
|
56
|
+
"heartbeat", "maxConcurrent", "compaction", "gateway",
|
|
57
|
+
]);
|
|
58
|
+
const ALLOWED_GATEWAY_KEYS = new Set(["port", "mode", "bind"]);
|
|
59
|
+
const VALID_MODES = new Set(["local", "remote", "hybrid"]);
|
|
60
|
+
const VALID_BINDS = new Set(["loopback", "tailscale", "any"]);
|
|
61
|
+
|
|
55
62
|
export async function PATCH(request: NextRequest) {
|
|
56
63
|
try {
|
|
57
64
|
const body = await request.json();
|
|
65
|
+
|
|
66
|
+
// Reject unknown top-level keys
|
|
67
|
+
const unknownKeys = Object.keys(body).filter((k) => !ALLOWED_TOP_LEVEL_KEYS.has(k));
|
|
68
|
+
if (unknownKeys.length > 0) {
|
|
69
|
+
return NextResponse.json(
|
|
70
|
+
{ error: `Unknown keys: ${unknownKeys.join(", ")}` },
|
|
71
|
+
{ status: 400 }
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate gateway sub-object if present
|
|
76
|
+
if (body.gateway !== undefined) {
|
|
77
|
+
if (typeof body.gateway !== "object" || body.gateway === null || Array.isArray(body.gateway)) {
|
|
78
|
+
return NextResponse.json(
|
|
79
|
+
{ error: "gateway must be an object" },
|
|
80
|
+
{ status: 400 }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const unknownGw = Object.keys(body.gateway).filter((k) => !ALLOWED_GATEWAY_KEYS.has(k));
|
|
84
|
+
if (unknownGw.length > 0) {
|
|
85
|
+
return NextResponse.json(
|
|
86
|
+
{ error: `Unknown gateway keys: ${unknownGw.join(", ")}` },
|
|
87
|
+
{ status: 400 }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (body.gateway.port !== undefined) {
|
|
91
|
+
if (!Number.isInteger(body.gateway.port) || body.gateway.port < 1 || body.gateway.port > 65535) {
|
|
92
|
+
return NextResponse.json(
|
|
93
|
+
{ error: "gateway.port must be an integer between 1 and 65535" },
|
|
94
|
+
{ status: 400 }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (body.gateway.mode !== undefined && !VALID_MODES.has(body.gateway.mode)) {
|
|
99
|
+
return NextResponse.json(
|
|
100
|
+
{ error: `gateway.mode must be one of: ${[...VALID_MODES].join(", ")}` },
|
|
101
|
+
{ status: 400 }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (body.gateway.bind !== undefined && !VALID_BINDS.has(body.gateway.bind)) {
|
|
105
|
+
return NextResponse.json(
|
|
106
|
+
{ error: `gateway.bind must be one of: ${[...VALID_BINDS].join(", ")}` },
|
|
107
|
+
{ status: 400 }
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
58
112
|
const config = await loadConfig();
|
|
59
113
|
|
|
60
114
|
if (!config.agents) config.agents = {};
|
|
@@ -79,6 +133,14 @@ export async function PATCH(request: NextRequest) {
|
|
|
79
133
|
config.agents.defaults.compaction = body.compaction;
|
|
80
134
|
}
|
|
81
135
|
|
|
136
|
+
// Gateway settings
|
|
137
|
+
if (body.gateway !== undefined) {
|
|
138
|
+
if (!config.gateway) config.gateway = {};
|
|
139
|
+
if (body.gateway.port !== undefined) config.gateway.port = body.gateway.port;
|
|
140
|
+
if (body.gateway.mode !== undefined) config.gateway.mode = body.gateway.mode;
|
|
141
|
+
if (body.gateway.bind !== undefined) config.gateway.bind = body.gateway.bind;
|
|
142
|
+
}
|
|
143
|
+
|
|
82
144
|
await saveConfig(config);
|
|
83
145
|
|
|
84
146
|
return NextResponse.json({ ok: true });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { getDb } from "@/lib/db";
|
|
3
|
+
import { validatePathParam } from "@/lib/config";
|
|
3
4
|
import { soulEvolutionLog } from "@/lib/db/schema";
|
|
4
5
|
import { eq, desc } from "drizzle-orm";
|
|
5
6
|
import fs from "fs/promises";
|
|
@@ -31,7 +32,12 @@ export async function GET(
|
|
|
31
32
|
{ params }: { params: Promise<{ id: string }> }
|
|
32
33
|
) {
|
|
33
34
|
try {
|
|
34
|
-
|
|
35
|
+
let soulId: string;
|
|
36
|
+
try {
|
|
37
|
+
soulId = validatePathParam((await params).id);
|
|
38
|
+
} catch {
|
|
39
|
+
return NextResponse.json({ error: "Invalid soul ID" }, { status: 400 });
|
|
40
|
+
}
|
|
35
41
|
const { searchParams } = new URL(request.url);
|
|
36
42
|
const status = searchParams.get("status") || "pending";
|
|
37
43
|
|
|
@@ -63,7 +69,10 @@ export async function GET(
|
|
|
63
69
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
64
70
|
fullEvents = lines.map((line) => JSON.parse(line));
|
|
65
71
|
} catch (error) {
|
|
66
|
-
// events.jsonl might be empty
|
|
72
|
+
// events.jsonl might be empty, not exist yet, or contain malformed lines
|
|
73
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
74
|
+
console.warn("Failed to parse events.jsonl:", error);
|
|
75
|
+
}
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
// Merge DB records with full event details
|
|
@@ -93,7 +102,12 @@ export async function POST(
|
|
|
93
102
|
{ params }: { params: Promise<{ id: string }> }
|
|
94
103
|
) {
|
|
95
104
|
try {
|
|
96
|
-
|
|
105
|
+
let soulId: string;
|
|
106
|
+
try {
|
|
107
|
+
soulId = validatePathParam((await params).id);
|
|
108
|
+
} catch {
|
|
109
|
+
return NextResponse.json({ error: "Invalid soul ID" }, { status: 400 });
|
|
110
|
+
}
|
|
97
111
|
const event: EvolutionEvent = await request.json();
|
|
98
112
|
|
|
99
113
|
const db = getDb();
|
|
@@ -133,7 +147,12 @@ export async function PATCH(
|
|
|
133
147
|
{ params }: { params: Promise<{ id: string }> }
|
|
134
148
|
) {
|
|
135
149
|
try {
|
|
136
|
-
|
|
150
|
+
let soulId: string;
|
|
151
|
+
try {
|
|
152
|
+
soulId = validatePathParam((await params).id);
|
|
153
|
+
} catch {
|
|
154
|
+
return NextResponse.json({ error: "Invalid soul ID" }, { status: 400 });
|
|
155
|
+
}
|
|
137
156
|
const { searchParams } = new URL(request.url);
|
|
138
157
|
const eventId = searchParams.get("eventId");
|
|
139
158
|
|
|
@@ -158,11 +177,20 @@ export async function PATCH(
|
|
|
158
177
|
"events.jsonl"
|
|
159
178
|
);
|
|
160
179
|
const content = await fs.readFile(eventsPath, "utf-8");
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
180
|
+
let events: EvolutionEvent[];
|
|
181
|
+
try {
|
|
182
|
+
events = content
|
|
183
|
+
.trim()
|
|
184
|
+
.split("\n")
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.map((line) => JSON.parse(line));
|
|
187
|
+
} catch (parseErr) {
|
|
188
|
+
console.error("Malformed JSONL in events.jsonl:", parseErr);
|
|
189
|
+
return NextResponse.json(
|
|
190
|
+
{ error: "Failed to parse evolution events file (malformed JSONL)" },
|
|
191
|
+
{ status: 500 }
|
|
192
|
+
);
|
|
193
|
+
}
|
|
166
194
|
const event = events.find((e: EvolutionEvent) => e.eventId === eventId);
|
|
167
195
|
|
|
168
196
|
if (!event) {
|
|
@@ -170,15 +198,25 @@ export async function PATCH(
|
|
|
170
198
|
}
|
|
171
199
|
|
|
172
200
|
// Apply change (e.g., update genes.json)
|
|
201
|
+
const safeTarget = validatePathParam(event.proposedChange.target);
|
|
173
202
|
const targetPath = path.join(
|
|
174
203
|
SOULS_DIR,
|
|
175
204
|
soulId,
|
|
176
205
|
"evolution",
|
|
177
|
-
|
|
206
|
+
safeTarget
|
|
178
207
|
);
|
|
179
208
|
|
|
180
209
|
if (event.proposedChange.action === "add") {
|
|
181
|
-
|
|
210
|
+
let existing;
|
|
211
|
+
try {
|
|
212
|
+
existing = JSON.parse(await fs.readFile(targetPath, "utf-8"));
|
|
213
|
+
} catch (parseErr) {
|
|
214
|
+
console.error(`Malformed JSON in ${targetPath}:`, parseErr);
|
|
215
|
+
return NextResponse.json(
|
|
216
|
+
{ error: `Failed to parse ${event.proposedChange.target} (malformed JSON)` },
|
|
217
|
+
{ status: 500 }
|
|
218
|
+
);
|
|
219
|
+
}
|
|
182
220
|
if (event.proposedChange.target === "genes.json") {
|
|
183
221
|
existing.genes.push(event.proposedChange.content);
|
|
184
222
|
}
|
|
@@ -189,7 +227,6 @@ export async function PATCH(
|
|
|
189
227
|
// Sanitize all user-derived inputs: eventId, soulId, reviewedBy, event.summary
|
|
190
228
|
// could all contain shell metacharacters if crafted maliciously.
|
|
191
229
|
const safeBranch = `evolution/${eventId.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
192
|
-
const safeTarget = event.proposedChange.target.replace(/[^a-zA-Z0-9._/-]/g, "_");
|
|
193
230
|
const commitMessage = [
|
|
194
231
|
`evolution(${eventId}): ${event.summary}`,
|
|
195
232
|
"",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { getDb } from "@/lib/db";
|
|
3
|
+
import { validatePathParam } from "@/lib/config";
|
|
3
4
|
import { soulSpawns } from "@/lib/db/schema";
|
|
4
5
|
import fs from "fs/promises";
|
|
5
6
|
import path from "path";
|
|
@@ -38,7 +39,12 @@ export async function POST(
|
|
|
38
39
|
{ params }: { params: Promise<{ id: string }> }
|
|
39
40
|
) {
|
|
40
41
|
try {
|
|
41
|
-
|
|
42
|
+
let soulId: string;
|
|
43
|
+
try {
|
|
44
|
+
soulId = validatePathParam((await params).id);
|
|
45
|
+
} catch {
|
|
46
|
+
return NextResponse.json({ error: "Invalid soul ID" }, { status: 400 });
|
|
47
|
+
}
|
|
42
48
|
const body: PromptRequest = await request.json();
|
|
43
49
|
|
|
44
50
|
const soulDir = path.join(SOULS_DIR, soulId);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { getDb } from "@/lib/db";
|
|
3
|
+
import { validatePathParam } from "@/lib/config";
|
|
3
4
|
import { soulEvolutionLog } from "@/lib/db/schema";
|
|
4
5
|
import { eq } from "drizzle-orm";
|
|
5
6
|
import fs from "fs/promises";
|
|
@@ -19,9 +20,20 @@ export async function POST(
|
|
|
19
20
|
{ params }: { params: Promise<{ id: string }> }
|
|
20
21
|
) {
|
|
21
22
|
try {
|
|
22
|
-
|
|
23
|
+
let sourceSoulId: string;
|
|
24
|
+
try {
|
|
25
|
+
sourceSoulId = validatePathParam((await params).id);
|
|
26
|
+
} catch {
|
|
27
|
+
return NextResponse.json({ error: "Invalid source soul ID" }, { status: 400 });
|
|
28
|
+
}
|
|
23
29
|
const body: PropagateRequest = await request.json();
|
|
24
|
-
|
|
30
|
+
let targetSoulId: string;
|
|
31
|
+
try {
|
|
32
|
+
targetSoulId = validatePathParam(body.targetSoulId);
|
|
33
|
+
} catch {
|
|
34
|
+
return NextResponse.json({ error: "Invalid target soul ID" }, { status: 400 });
|
|
35
|
+
}
|
|
36
|
+
const { sourceEventId } = body;
|
|
25
37
|
|
|
26
38
|
// Validate target soul exists
|
|
27
39
|
const targetSoulDir = path.join(SOULS_DIR, targetSoulId);
|
|
@@ -63,10 +75,17 @@ export async function POST(
|
|
|
63
75
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
64
76
|
const events: EvolutionEventEntry[] = lines.map((line) => JSON.parse(line));
|
|
65
77
|
sourceEvent = events.find((e) => e.eventId === sourceEventId) ?? null;
|
|
66
|
-
} catch {
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
80
|
+
return NextResponse.json(
|
|
81
|
+
{ error: "Source events file not found" },
|
|
82
|
+
{ status: 404 }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
console.error("Failed to parse source events.jsonl:", error);
|
|
67
86
|
return NextResponse.json(
|
|
68
|
-
{ error: "
|
|
69
|
-
{ status:
|
|
87
|
+
{ error: "Failed to parse source events file (malformed JSONL)" },
|
|
88
|
+
{ status: 500 }
|
|
70
89
|
);
|
|
71
90
|
}
|
|
72
91
|
|
|
@@ -94,9 +94,10 @@ export async function POST(request: NextRequest) {
|
|
|
94
94
|
return NextResponse.json(newSoul, { status: 201 });
|
|
95
95
|
} catch (error) {
|
|
96
96
|
console.error("Failed to register soul:", error);
|
|
97
|
+
const status = error instanceof SyntaxError ? 400 : 500;
|
|
97
98
|
return NextResponse.json(
|
|
98
|
-
{ error: "Failed to register soul" },
|
|
99
|
-
{ status
|
|
99
|
+
{ error: status === 400 ? String(error) : "Failed to register soul" },
|
|
100
|
+
{ status }
|
|
100
101
|
);
|
|
101
102
|
}
|
|
102
103
|
}
|
|
@@ -133,9 +134,10 @@ export async function PATCH(request: NextRequest) {
|
|
|
133
134
|
return NextResponse.json(registry.souls[soulIndex]);
|
|
134
135
|
} catch (error) {
|
|
135
136
|
console.error("Failed to update soul:", error);
|
|
137
|
+
const status = error instanceof SyntaxError ? 400 : 500;
|
|
136
138
|
return NextResponse.json(
|
|
137
|
-
{ error: "Failed to update soul" },
|
|
138
|
-
{ status
|
|
139
|
+
{ error: status === 400 ? String(error) : "Failed to update soul" },
|
|
140
|
+
{ status }
|
|
139
141
|
);
|
|
140
142
|
}
|
|
141
143
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { getDb } from "@/lib/db";
|
|
3
|
+
import { validatePathParam } from "@/lib/config";
|
|
3
4
|
import { tasks, soulHandoffs } from "@/lib/db/schema";
|
|
4
5
|
import { eq } from "drizzle-orm";
|
|
5
6
|
import fs from "fs/promises";
|
|
@@ -27,7 +28,12 @@ export async function POST(
|
|
|
27
28
|
{ params }: { params: Promise<{ id: string }> }
|
|
28
29
|
) {
|
|
29
30
|
try {
|
|
30
|
-
|
|
31
|
+
let taskId: string;
|
|
32
|
+
try {
|
|
33
|
+
taskId = validatePathParam((await params).id);
|
|
34
|
+
} catch {
|
|
35
|
+
return NextResponse.json({ error: "Invalid task ID" }, { status: 400 });
|
|
36
|
+
}
|
|
31
37
|
const body: HandoffRequest = await request.json();
|
|
32
38
|
|
|
33
39
|
const db = getDb();
|
|
@@ -9,6 +9,12 @@ import { gatewayNotify } from "@/lib/gateway-notify";
|
|
|
9
9
|
import { AGENT_NAME, HUMAN_NAME } from "@/lib/config";
|
|
10
10
|
import { getNats, sc } from "@/lib/nats";
|
|
11
11
|
|
|
12
|
+
/** Safely parse a JSON string from a DB field, returning fallback on failure. */
|
|
13
|
+
function safeParse(json: string | null, fallback: unknown = []): unknown {
|
|
14
|
+
if (!json) return fallback;
|
|
15
|
+
try { return JSON.parse(json); } catch { return fallback; }
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
/**
|
|
13
19
|
* Push a notification message to the OpenClaw TUI via gateway chat.send + abort.
|
|
14
20
|
* Fire-and-forget — does not block the API response.
|
|
@@ -130,6 +136,18 @@ export async function PATCH(
|
|
|
130
136
|
const body = await request.json();
|
|
131
137
|
const now = new Date().toISOString();
|
|
132
138
|
|
|
139
|
+
// Status validation
|
|
140
|
+
const VALID_STATUSES = new Set([
|
|
141
|
+
"not started", "queued", "ready", "submitted", "running",
|
|
142
|
+
"blocked", "waiting-user", "done", "cancelled", "archived",
|
|
143
|
+
]);
|
|
144
|
+
if (body.status && !VALID_STATUSES.has(body.status)) {
|
|
145
|
+
return NextResponse.json(
|
|
146
|
+
{ error: `Invalid status: ${body.status}` },
|
|
147
|
+
{ status: 400 }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
133
151
|
// Transition guard: block execution mode changes on in-flight mesh tasks
|
|
134
152
|
if (body.execution !== undefined && body.execution !== existing.execution) {
|
|
135
153
|
if (existing.meshTaskId) {
|
|
@@ -337,10 +355,8 @@ export async function PATCH(
|
|
|
337
355
|
|
|
338
356
|
return NextResponse.json({
|
|
339
357
|
...updated,
|
|
340
|
-
successCriteria: updated?.successCriteria
|
|
341
|
-
|
|
342
|
-
: [],
|
|
343
|
-
artifacts: updated?.artifacts ? JSON.parse(updated.artifacts) : [],
|
|
358
|
+
successCriteria: safeParse(updated?.successCriteria ?? null),
|
|
359
|
+
artifacts: safeParse(updated?.artifacts ?? null),
|
|
344
360
|
});
|
|
345
361
|
} catch (err) {
|
|
346
362
|
console.error("PATCH /api/tasks/[id] error:", err);
|