wogiflow 2.6.1 → 2.6.2
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.
|
@@ -247,7 +247,14 @@ function processSuggestedTask(workspaceRoot, message) {
|
|
|
247
247
|
|
|
248
248
|
try {
|
|
249
249
|
const ready = JSON.parse(fs.readFileSync(readyPath, 'utf-8'));
|
|
250
|
-
|
|
250
|
+
// Use generateTaskId when available, fallback to random (finding-008)
|
|
251
|
+
let taskId;
|
|
252
|
+
try {
|
|
253
|
+
const { generateTaskId } = require('../scripts/flow-utils');
|
|
254
|
+
taskId = generateTaskId(message.suggestedTask.title || message.subject);
|
|
255
|
+
} catch (_err) {
|
|
256
|
+
taskId = 'wf-' + crypto.randomBytes(4).toString('hex');
|
|
257
|
+
}
|
|
251
258
|
|
|
252
259
|
const task = {
|
|
253
260
|
id: taskId,
|
package/lib/workspace.js
CHANGED
|
@@ -22,6 +22,22 @@ const path = require('node:path');
|
|
|
22
22
|
// Constants
|
|
23
23
|
// ============================================================
|
|
24
24
|
|
|
25
|
+
// Proto-pollution safe JSON parse (finding-007)
|
|
26
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
27
|
+
function safeParseJson(str, fallback) {
|
|
28
|
+
try {
|
|
29
|
+
const obj = JSON.parse(str);
|
|
30
|
+
if (obj && typeof obj === 'object') {
|
|
31
|
+
for (const key of Object.keys(obj)) {
|
|
32
|
+
if (DANGEROUS_KEYS.has(key)) delete obj[key];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return obj;
|
|
36
|
+
} catch (_err) {
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
const WORKSPACE_CONFIG_FILE = 'wogi-workspace.json';
|
|
26
42
|
const WORKSPACE_DIR = '.workspace';
|
|
27
43
|
const WORKSPACE_DIRS = [
|
|
@@ -102,7 +118,7 @@ function readMemberMetadata(workflowPath) {
|
|
|
102
118
|
try {
|
|
103
119
|
if (fs.existsSync(filePath)) {
|
|
104
120
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
105
|
-
metadata[key] = json ?
|
|
121
|
+
metadata[key] = json ? safeParseJson(content, {}) : content;
|
|
106
122
|
}
|
|
107
123
|
} catch (_err) {
|
|
108
124
|
// Non-critical — skip unreadable files
|
|
@@ -114,7 +130,7 @@ function readMemberMetadata(workflowPath) {
|
|
|
114
130
|
const filePath = path.join(statePath, file);
|
|
115
131
|
try {
|
|
116
132
|
if (fs.existsSync(filePath)) {
|
|
117
|
-
metadata[key] =
|
|
133
|
+
metadata[key] = safeParseJson(fs.readFileSync(filePath, 'utf-8'), {});
|
|
118
134
|
}
|
|
119
135
|
} catch (_err) {
|
|
120
136
|
// Non-critical
|
|
@@ -227,7 +243,7 @@ function detectStack(metadata, memberPath) {
|
|
|
227
243
|
const pkgPath = path.join(memberPath, 'package.json');
|
|
228
244
|
if (fs.existsSync(pkgPath)) {
|
|
229
245
|
try {
|
|
230
|
-
const pkg =
|
|
246
|
+
const pkg = safeParseJson(fs.readFileSync(pkgPath, 'utf-8'), {});
|
|
231
247
|
if (pkg.dependencies?.typescript || pkg.devDependencies?.typescript) {
|
|
232
248
|
stack.language = 'TypeScript';
|
|
233
249
|
} else {
|
|
@@ -910,10 +926,24 @@ function generateMemberMcpConfigs(workspaceRoot, config) {
|
|
|
910
926
|
continue;
|
|
911
927
|
}
|
|
912
928
|
|
|
929
|
+
// Reserved name check (finding-003) — 'manager' is used as the orchestrator identity
|
|
930
|
+
if (name === 'manager') {
|
|
931
|
+
console.error(` ✗ ${name}: 'manager' is a reserved name — rename this member repo`);
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
|
|
913
935
|
const memberPath = path.resolve(workspaceRoot, config.members[name]?.path || `./${name}`);
|
|
914
936
|
|
|
915
|
-
// Path traversal guard
|
|
916
|
-
|
|
937
|
+
// Path traversal guard with symlink resolution (finding-006)
|
|
938
|
+
let realRoot, realMember;
|
|
939
|
+
try {
|
|
940
|
+
realRoot = fs.realpathSync(workspaceRoot);
|
|
941
|
+
realMember = fs.realpathSync(memberPath);
|
|
942
|
+
} catch (_err) {
|
|
943
|
+
realRoot = workspaceRoot;
|
|
944
|
+
realMember = memberPath;
|
|
945
|
+
}
|
|
946
|
+
if (!realMember.startsWith(realRoot + path.sep) && realMember !== realRoot) {
|
|
917
947
|
console.error(` ✗ ${name}: path escapes workspace root (${config.members[name]?.path}) — skipping`);
|
|
918
948
|
continue;
|
|
919
949
|
}
|
package/package.json
CHANGED
|
@@ -282,62 +282,8 @@ async function handleTaskCompleted(input) {
|
|
|
282
282
|
} catch (_err) {
|
|
283
283
|
// Non-critical - registry manager may not be available
|
|
284
284
|
}
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
try {
|
|
288
|
-
const workspaceRoot = process.env.WOGI_WORKSPACE_ROOT;
|
|
289
|
-
const repoName = process.env.WOGI_REPO_NAME || path.basename(process.cwd());
|
|
290
|
-
const messagesDir = path.join(workspaceRoot, '.workspace', 'messages');
|
|
291
|
-
fs.mkdirSync(messagesDir, { recursive: true });
|
|
292
|
-
|
|
293
|
-
// Build a summary from available task data
|
|
294
|
-
const summary = [];
|
|
295
|
-
if (completedTask.title) summary.push(`**Task**: ${completedTask.title}`);
|
|
296
|
-
if (completedTask.type) summary.push(`**Type**: ${completedTask.type}`);
|
|
297
|
-
if (input.changedFiles?.length) summary.push(`**Files changed**: ${input.changedFiles.join(', ')}`);
|
|
298
|
-
if (input.summary) summary.push(`**Summary**: ${input.summary}`);
|
|
299
|
-
|
|
300
|
-
// Read the last request-log entry for richer context
|
|
301
|
-
try {
|
|
302
|
-
const logPath = path.join(PATHS.root, 'request-log.md');
|
|
303
|
-
if (fs.existsSync(logPath)) {
|
|
304
|
-
const logContent = fs.readFileSync(logPath, 'utf-8');
|
|
305
|
-
const lastEntry = logContent.split(/^### R-/m).pop();
|
|
306
|
-
if (lastEntry && lastEntry.length < 2000) {
|
|
307
|
-
summary.push(`**Log entry**:\n${lastEntry.trim()}`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
} catch (_err) {
|
|
311
|
-
// Non-critical
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const message = {
|
|
315
|
-
id: 'msg-' + require('node:crypto').randomBytes(4).toString('hex'),
|
|
316
|
-
from: repoName,
|
|
317
|
-
to: 'manager',
|
|
318
|
-
type: 'task-complete',
|
|
319
|
-
priority: 'medium',
|
|
320
|
-
timestamp: new Date().toISOString(),
|
|
321
|
-
subject: `Task completed: ${completedTask.title || completedTask.id}`,
|
|
322
|
-
body: summary.join('\n') || `Task ${completedTask.id} completed successfully.`,
|
|
323
|
-
taskId: completedTask.id,
|
|
324
|
-
actionRequired: false,
|
|
325
|
-
status: 'pending'
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
const msgPath = path.join(messagesDir, `${message.id}.json`);
|
|
329
|
-
fs.writeFileSync(msgPath, JSON.stringify(message, null, 2));
|
|
330
|
-
|
|
331
|
-
if (process.env.DEBUG) {
|
|
332
|
-
console.error(`[Task Completed] Workspace message written: ${msgPath}`);
|
|
333
|
-
}
|
|
334
|
-
} catch (err) {
|
|
335
|
-
// Non-critical — workspace messaging is best-effort
|
|
336
|
-
if (process.env.DEBUG) {
|
|
337
|
-
console.error(`[Task Completed] Workspace message failed: ${err.message}`);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
285
|
+
// Workspace notifications are handled by the Stop hook (via HTTP to manager port).
|
|
286
|
+
// Removed duplicate file-based notification here to prevent double messages (finding-004).
|
|
341
287
|
|
|
342
288
|
// Check pending queue — notify user if items are waiting
|
|
343
289
|
try {
|
|
@@ -62,21 +62,24 @@ runHook('Stop', async ({ parsedInput }) => {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Workspace worker: send results to manager via HTTP when stopping.
|
|
65
|
-
// Uses
|
|
66
|
-
// The async http.request approach was unreliable — process exited before request completed.
|
|
65
|
+
// Uses execFileSync with array args to avoid shell injection (finding-001).
|
|
67
66
|
if (process.env.WOGI_MANAGER_PORT && process.env.WOGI_REPO_NAME && process.env.WOGI_REPO_NAME !== 'manager') {
|
|
68
67
|
try {
|
|
69
|
-
const { execSync } = require('node:child_process');
|
|
70
|
-
const fs = require('node:fs');
|
|
68
|
+
const { execFileSync, execSync } = require('node:child_process');
|
|
71
69
|
const path = require('node:path');
|
|
70
|
+
const VALID_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
72
71
|
const repoName = process.env.WOGI_REPO_NAME;
|
|
73
|
-
const managerPort = process.env.WOGI_MANAGER_PORT;
|
|
72
|
+
const managerPort = parseInt(process.env.WOGI_MANAGER_PORT, 10);
|
|
73
|
+
|
|
74
|
+
// Validate inputs before using them (finding-001, finding-002)
|
|
75
|
+
if (!VALID_NAME.test(repoName) || !Number.isInteger(managerPort) || managerPort < 1024 || managerPort > 65535) {
|
|
76
|
+
throw new Error(`Invalid WOGI_REPO_NAME or WOGI_MANAGER_PORT`);
|
|
77
|
+
}
|
|
74
78
|
|
|
75
79
|
// Build summary from available state
|
|
76
80
|
const summaryParts = [];
|
|
77
81
|
const { PATHS, safeJsonParse } = require('../../flow-utils');
|
|
78
82
|
|
|
79
|
-
// Get current/recently completed task info
|
|
80
83
|
const ready = safeJsonParse(path.join(PATHS.state, 'ready.json'), {});
|
|
81
84
|
const recentTask = (ready.recentlyCompleted || [])[0];
|
|
82
85
|
const inProgressTask = (ready.inProgress || [])[0];
|
|
@@ -87,7 +90,6 @@ runHook('Stop', async ({ parsedInput }) => {
|
|
|
87
90
|
if (task.type) summaryParts.push(`**Type**: ${task.type}`);
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
// Get changed files
|
|
91
93
|
try {
|
|
92
94
|
const diff = execSync('git diff --name-only HEAD 2>/dev/null || true', { cwd: PATHS.root, encoding: 'utf-8' }).trim();
|
|
93
95
|
const staged = execSync('git diff --name-only --staged 2>/dev/null || true', { cwd: PATHS.root, encoding: 'utf-8' }).trim();
|
|
@@ -97,22 +99,19 @@ runHook('Stop', async ({ parsedInput }) => {
|
|
|
97
99
|
}
|
|
98
100
|
} catch (_err) { /* non-critical */ }
|
|
99
101
|
|
|
100
|
-
const body = summaryParts.join('\n') ||
|
|
102
|
+
const body = summaryParts.join('\n') || `Work completed by ${repoName}.`;
|
|
101
103
|
|
|
102
|
-
//
|
|
104
|
+
// execFileSync with array args — no shell interpretation (finding-001 fix)
|
|
103
105
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
106
|
+
execFileSync('curl', [
|
|
107
|
+
'-s', '-X', 'POST',
|
|
108
|
+
`http://127.0.0.1:${managerPort}`,
|
|
109
|
+
'-H', 'Content-Type: text/plain',
|
|
110
|
+
'-H', `X-Wogi-From: ${repoName}`,
|
|
111
|
+
'--data-binary', '@-'
|
|
112
|
+
], { input: body, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
111
113
|
} catch (_err) {
|
|
112
114
|
// Manager might be offline — that's OK
|
|
113
|
-
if (process.env.DEBUG) {
|
|
114
|
-
console.error(`[Stop] curl to manager:${managerPort} failed: ${_err.message}`);
|
|
115
|
-
}
|
|
116
115
|
}
|
|
117
116
|
} catch (err) {
|
|
118
117
|
if (process.env.DEBUG) {
|