wogiflow 2.6.0 → 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
  }
@@ -990,22 +1020,32 @@ description: "Workspace worker rules — reply to manager after every channel ta
990
1020
 
991
1021
  You are **${name}** in a Wogi Workspace managed by an orchestrator.
992
1022
 
993
- ## CRITICAL: Always Reply to the Manager
1023
+ ## CRITICAL: Always Reply to the Manager (port ${config.channels.managerPort || (config.channels.basePort - 1)})
994
1024
 
995
- When you receive a message via the workspace channel (appears as a channel notification), you MUST send results back to the manager after completing the work. **The user only sees the manager terminal — if you don't reply, they never see your results.**
1025
+ When you receive a message via the workspace channel, you MUST send results back to the manager after completing the work. **The user only sees the manager terminal — if you don't reply, they never see your results.**
996
1026
 
997
- Use the \`workspace_send_message\` MCP tool:
1027
+ **PRIMARY method curl to manager port (always works, even if MCP tools haven't loaded yet):**
1028
+
1029
+ \`\`\`bash
1030
+ curl -s -X POST http://localhost:${config.channels.managerPort || (config.channels.basePort - 1)} -H "Content-Type: text/plain" -H "X-Wogi-From: ${name}" -d "## Results
1031
+
1032
+ [your findings/summary here]"
1033
+ \`\`\`
1034
+
1035
+ **ALTERNATIVE — workspace_send_message MCP tool (if available):**
998
1036
 
999
1037
  \`\`\`
1000
1038
  workspace_send_message(to: "manager", message: "## Results\\n\\n[your findings/summary here]")
1001
1039
  \`\`\`
1002
1040
 
1003
- This is NON-OPTIONAL. Every channel-dispatched task MUST end with a workspace_send_message to "manager".
1041
+ If the MCP tool is not available (shows "MCP servers still connecting"), use the curl command. **Do NOT write JSON files to .workspace/messages/ directly — that doesn't notify the manager.**
1042
+
1043
+ This is NON-OPTIONAL. Every channel-dispatched task MUST end with a reply to the manager.
1004
1044
 
1005
1045
  ## Peer Communication
1006
1046
 
1007
- You can talk to peer repos using the same tool:
1008
- ${peerNames.map(p => `- \`workspace_send_message(to: "${p}", message: "...")\``).join('\n')}
1047
+ You can talk to peer repos via curl or the MCP tool:
1048
+ ${peerNames.map(p => `- \`curl -s -X POST http://localhost:${channelMembers[p]?.port || '???'} -H "X-Wogi-From: ${name}" -d "your message"\``).join('\n')}
1009
1049
 
1010
1050
  ## Autonomous Mode — Auto-Approve Everything
1011
1051
 
@@ -1024,7 +1064,7 @@ Only send a question to the manager (instead of results) when:
1024
1064
  - The task requires **deleting user data** or making irreversible changes
1025
1065
  - You are genuinely **stuck** and cannot proceed
1026
1066
 
1027
- To escalate: \`workspace_send_message(to: "manager", message: "## Need Decision\\n\\n[describe the choice and options]")\`
1067
+ To escalate: \`curl -s -X POST http://localhost:${config.channels.managerPort || (config.channels.basePort - 1)} -H "X-Wogi-From: ${name}" -d "## Need Decision: [describe the choice and options]"\`
1028
1068
 
1029
1069
  For everything else — just do the work and report results.
1030
1070
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.6.0",
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 {
@@ -61,113 +61,61 @@ runHook('Stop', async ({ parsedInput }) => {
61
61
  };
62
62
  }
63
63
 
64
- // Workspace worker: auto-write results back to manager when stopping
65
- // This runs in the hook (not the AI), so it's guaranteed to execute.
66
- // The AI can't be relied on to call workspace_send_message — the stop
67
- // hook fires before the AI gets a chance, or the AI forgets.
68
- if (process.env.WOGI_WORKSPACE_ROOT && process.env.WOGI_REPO_NAME) {
64
+ // Workspace worker: send results to manager via HTTP when stopping.
65
+ // Uses execFileSync with array args to avoid shell injection (finding-001).
66
+ if (process.env.WOGI_MANAGER_PORT && process.env.WOGI_REPO_NAME && process.env.WOGI_REPO_NAME !== 'manager') {
69
67
  try {
70
- const fs = require('node:fs');
68
+ const { execFileSync, execSync } = require('node:child_process');
71
69
  const path = require('node:path');
72
- const crypto = require('node:crypto');
73
- const workspaceRoot = process.env.WOGI_WORKSPACE_ROOT;
70
+ const VALID_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
74
71
  const repoName = process.env.WOGI_REPO_NAME;
75
- const messagesDir = path.join(workspaceRoot, '.workspace', 'messages');
76
- fs.mkdirSync(messagesDir, { recursive: true });
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
+ }
77
78
 
78
79
  // Build summary from available state
79
- const summary = [];
80
+ const summaryParts = [];
80
81
  const { PATHS, safeJsonParse } = require('../../flow-utils');
81
82
 
82
- // Get current/recently completed task info
83
83
  const ready = safeJsonParse(path.join(PATHS.state, 'ready.json'), {});
84
84
  const recentTask = (ready.recentlyCompleted || [])[0];
85
85
  const inProgressTask = (ready.inProgress || [])[0];
86
86
  const task = recentTask || inProgressTask;
87
87
 
88
88
  if (task) {
89
- summary.push(`**Task**: ${task.title || task.id}`);
90
- if (task.type) summary.push(`**Type**: ${task.type}`);
89
+ summaryParts.push(`**Task**: ${task.title || task.id}`);
90
+ if (task.type) summaryParts.push(`**Type**: ${task.type}`);
91
91
  }
92
92
 
93
- // Get last request-log entry
94
- try {
95
- const logPath = path.join(PATHS.root, 'request-log.md');
96
- if (fs.existsSync(logPath)) {
97
- const stat = fs.statSync(logPath);
98
- if (stat.size < 200 * 1024) {
99
- const logContent = fs.readFileSync(logPath, 'utf-8');
100
- const parts = logContent.split(/^### R-/m);
101
- if (parts.length > 1) {
102
- const lastEntry = parts[parts.length - 1];
103
- if (lastEntry.length < 2000) {
104
- summary.push(`**Log entry**:\n### R-${lastEntry.trim()}`);
105
- }
106
- }
107
- }
108
- }
109
- } catch (_err) { /* non-critical */ }
110
-
111
- // Get git status for changed files
112
93
  try {
113
- const { execSync } = require('node:child_process');
114
94
  const diff = execSync('git diff --name-only HEAD 2>/dev/null || true', { cwd: PATHS.root, encoding: 'utf-8' }).trim();
115
95
  const staged = execSync('git diff --name-only --staged 2>/dev/null || true', { cwd: PATHS.root, encoding: 'utf-8' }).trim();
116
96
  const allChanged = [...new Set([...diff.split('\n'), ...staged.split('\n')].filter(Boolean))];
117
97
  if (allChanged.length > 0) {
118
- summary.push(`**Files changed**: ${allChanged.join(', ')}`);
98
+ summaryParts.push(`**Files changed**: ${allChanged.join(', ')}`);
119
99
  }
120
100
  } catch (_err) { /* non-critical */ }
121
101
 
122
- const msgId = 'msg-' + crypto.randomBytes(4).toString('hex');
123
- const message = {
124
- id: msgId,
125
- from: repoName,
126
- to: 'manager',
127
- type: 'task-complete',
128
- priority: 'medium',
129
- timestamp: new Date().toISOString(),
130
- subject: task ? `Completed: ${task.title || task.id}` : `Work completed by ${repoName}`,
131
- body: summary.join('\n') || `${repoName} finished processing.`,
132
- taskId: task?.id || null,
133
- actionRequired: false,
134
- status: 'pending'
135
- };
102
+ const body = summaryParts.join('\n') || `Work completed by ${repoName}.`;
136
103
 
137
- // Write to file (fallback / persistent record)
138
- fs.writeFileSync(path.join(messagesDir, `${msgId}.json`), JSON.stringify(message, null, 2));
139
-
140
- // Also HTTP POST to manager's channel port for real-time notification
141
- // This makes the message appear as a prompt in the manager's session immediately
142
- const managerPort = process.env.WOGI_MANAGER_PORT;
143
- if (managerPort && repoName !== 'manager') {
144
- try {
145
- const http = require('node:http');
146
- const body = Buffer.from(message.body, 'utf-8');
147
- const req = http.request({
148
- hostname: '127.0.0.1',
149
- port: parseInt(managerPort, 10),
150
- path: '/',
151
- method: 'POST',
152
- headers: {
153
- 'Content-Type': 'text/plain',
154
- 'Content-Length': body.byteLength,
155
- 'X-Wogi-From': repoName
156
- }
157
- });
158
- req.on('error', () => { /* best effort — file is the fallback */ });
159
- req.write(body);
160
- req.end();
161
- } catch (_err) { /* non-critical */ }
162
- }
163
-
164
- if (process.env.DEBUG) {
165
- console.error(`[Stop] Workspace message written: ${msgId}${managerPort ? ` + HTTP to manager:${managerPort}` : ''}`);
104
+ // execFileSync with array args no shell interpretation (finding-001 fix)
105
+ try {
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'] });
113
+ } catch (_err) {
114
+ // Manager might be offline — that's OK
166
115
  }
167
116
  } catch (err) {
168
- // Non-critical — best effort
169
117
  if (process.env.DEBUG) {
170
- console.error(`[Stop] Workspace message failed: ${err.message}`);
118
+ console.error(`[Stop] Workspace notification failed: ${err.message}`);
171
119
  }
172
120
  }
173
121
  }