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
- const taskId = 'wf-' + crypto.randomBytes(4).toString('hex');
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 ? JSON.parse(content) : content;
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] = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
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 = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
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 ensure member path is inside workspace root
916
- if (!memberPath.startsWith(workspaceRoot + path.sep) && memberPath !== workspaceRoot) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.6.1",
3
+ "version": "2.6.2",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -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
- // Write results back to workspace message bus if running as a workspace worker
286
- if (result.completed && process.env.WOGI_WORKSPACE_ROOT) {
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 synchronous curl to guarantee delivery before the hook process exits.
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') || `${repoName} finished processing.`;
102
+ const body = summaryParts.join('\n') || `Work completed by ${repoName}.`;
101
103
 
102
- // PRIMARY: Synchronous curl to manager port guaranteed to complete before exit
104
+ // execFileSync with array argsno shell interpretation (finding-001 fix)
103
105
  try {
104
- execSync(
105
- `curl -s -X POST http://127.0.0.1:${managerPort} -H "Content-Type: text/plain" -H "X-Wogi-From: ${repoName}" --data-binary @-`,
106
- { input: body, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
107
- );
108
- if (process.env.DEBUG) {
109
- console.error(`[Stop] Sent results to manager via curl :${managerPort}`);
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) {