wogiflow 2.29.0 → 2.29.1

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/lib/wogi-claude CHANGED
@@ -107,13 +107,78 @@ if [ "$__wogi_is_worker" -eq 1 ]; then
107
107
  fi
108
108
  fi
109
109
 
110
- # Resolve the empty-MCP config used for stripping. Persistent path so we don't
111
- # regenerate a tmpfile on every restart; living in .workflow/state/ keeps it
112
- # alongside other worker state.
110
+ # Resolve the channel-only MCP config used for stripping. Persistent path
111
+ # so we don't regenerate on every restart; living in .workflow/state/ keeps
112
+ # it alongside other worker state.
113
+ #
114
+ # IMPORTANT — REGRESSION FIX (audit-channel-transport-001):
115
+ #
116
+ # The original Story A (wf-8294d960) wrote `{"mcpServers":{}}` — fully
117
+ # empty — under the (unverified) assumption that workers don't need any
118
+ # MCP servers in worker mode. That assumption was WRONG: it stripped
119
+ # `wogi-workspace-channel` which IS the transport that the manager uses
120
+ # to dispatch tasks to workers via `workspace_send_message` (the manager
121
+ # HTTP-POSTs to the worker's channel-server port; with no MCP server,
122
+ # there's no listener, so dispatches silently fail with "connection
123
+ # refused"). Tier-3 evidence (end-to-end manager→worker dispatch) was
124
+ # never collected; only boot-latency was measured. Story B
125
+ # (wf-ab59f0e4) layered COMPLETION-SUMMARY routing on top of this
126
+ # broken transport without auditing the dependency.
127
+ #
128
+ # The proper fix: extract ONLY the `wogi-workspace-channel` entry from
129
+ # the worker's real `.mcp.json` and write a channel-only config. This
130
+ # preserves Story A's boot-speed win (claude.ai MCP integrations stay
131
+ # stripped) while keeping the workspace transport active. If the
132
+ # worker's `.mcp.json` doesn't define `wogi-workspace-channel` (e.g.
133
+ # this is not a workspace member), fall back to the empty MCP config
134
+ # (the strip is harmless in non-workspace contexts).
113
135
  __wogi_empty_mcp_config=""
114
136
  if [ "$__wogi_strip_mcp" -eq 1 ]; then
115
- __wogi_empty_mcp_config="${WOGI_WORKSPACE_ROOT:-$(pwd)}/.workflow/state/worker-empty-mcp.json"
116
- if [ ! -f "$__wogi_empty_mcp_config" ]; then
137
+ __wogi_empty_mcp_config="${WOGI_WORKSPACE_ROOT:-$(pwd)}/.workflow/state/worker-channel-only-mcp.json"
138
+ __wogi_member_mcp_path="$(pwd)/.mcp.json"
139
+ if command -v node >/dev/null 2>&1; then
140
+ # Use the dedicated helper (testable; see tests/flow-worker-mcp-strip.test.js).
141
+ # Extracts ONLY the wogi-workspace-channel entry from the worker's real
142
+ # .mcp.json so manager-side workspace_send_message dispatch keeps working.
143
+ __wogi_strip_helper=""
144
+ for __wogi_candidate in \
145
+ "$(dirname "$0")/../scripts/flow-worker-mcp-strip.js" \
146
+ "$(npm root -g 2>/dev/null)/wogiflow/scripts/flow-worker-mcp-strip.js" \
147
+ "$(pwd)/node_modules/wogiflow/scripts/flow-worker-mcp-strip.js"; do
148
+ if [ -f "$__wogi_candidate" ]; then
149
+ __wogi_strip_helper="$__wogi_candidate"
150
+ break
151
+ fi
152
+ done
153
+ if [ -n "$__wogi_strip_helper" ]; then
154
+ node "$__wogi_strip_helper" "$__wogi_member_mcp_path" "$__wogi_empty_mcp_config" 2>/dev/null || __wogi_strip_mcp=0
155
+ else
156
+ # Helper not found — fall back to inline extraction (legacy code path
157
+ # for installs that pre-date the helper script).
158
+ node -e '
159
+ const fs = require("fs");
160
+ const path = require("path");
161
+ const [src, out] = process.argv.slice(1);
162
+ let channelEntry = null;
163
+ try {
164
+ if (fs.existsSync(src)) {
165
+ const cfg = JSON.parse(fs.readFileSync(src, "utf-8"));
166
+ const ws = cfg && cfg.mcpServers && cfg.mcpServers["wogi-workspace-channel"];
167
+ if (ws) channelEntry = ws;
168
+ }
169
+ } catch (_e) {}
170
+ const payload = channelEntry
171
+ ? { mcpServers: { "wogi-workspace-channel": channelEntry } }
172
+ : { mcpServers: {} };
173
+ fs.mkdirSync(path.dirname(out), { recursive: true });
174
+ const tmp = out + ".tmp." + process.pid;
175
+ fs.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n");
176
+ fs.renameSync(tmp, out);
177
+ ' "$__wogi_member_mcp_path" "$__wogi_empty_mcp_config" 2>/dev/null || __wogi_strip_mcp=0
178
+ fi
179
+ else
180
+ # node missing — last-resort fallback. Empty config = manager dispatch
181
+ # will fail, but worker boot will still succeed.
117
182
  mkdir -p "$(dirname "$__wogi_empty_mcp_config")" 2>/dev/null
118
183
  printf '{"mcpServers":{}}\n' > "$__wogi_empty_mcp_config" 2>/dev/null || __wogi_strip_mcp=0
119
184
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.29.0",
3
+ "version": "2.29.1",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "flow": "./scripts/flow",
13
- "test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
13
+ "test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
14
14
  "test:syntax": "find scripts/ lib/ -name '*.js' -not -path '*/node_modules/*' -exec node --check {} +",
15
15
  "lint": "eslint scripts/ lib/ tests/",
16
16
  "lint:ci": "eslint scripts/ lib/ tests/ --max-warnings 0",
@@ -368,8 +368,9 @@ function invalidateConfigCache() {
368
368
  // Config Value Access
369
369
  // ============================================================
370
370
 
371
- // Dangerous property names that could lead to prototype pollution
372
- const DANGEROUS_CONFIG_PROPS = new Set(['__proto__', 'constructor', 'prototype']);
371
+ // Dangerous property names that could lead to prototype pollution.
372
+ // Consolidated to flow-io canonical (audit dup-002 / wf-9fc4970b).
373
+ const { DANGEROUS_KEYS: DANGEROUS_CONFIG_PROPS } = require('./flow-io');
373
374
 
374
375
  /**
375
376
  * Validate config path doesn't contain dangerous property names
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  const path = require('node:path');
19
+ const { DANGEROUS_KEYS } = require('./flow-io');
19
20
  const {
20
21
  PATHS,
21
22
  safeJsonParse,
@@ -695,11 +696,11 @@ function loadPendingCorrections() {
695
696
  // SEC-005 fix (2026-04-13): recursive prototype-pollution check for
696
697
  // array-rooted JSON. Returns true if __proto__/constructor/prototype found.
697
698
  function hasDangerousKeys(value) {
698
- const dangerous = new Set(['__proto__', 'constructor', 'prototype']);
699
+ // Consolidated to flow-io canonical DANGEROUS_KEYS (audit dup-002 / wf-9fc4970b).
699
700
  const visit = (node, depth) => {
700
701
  if (depth > 8 || node === null || typeof node !== 'object') return false;
701
702
  for (const key of Object.getOwnPropertyNames(node)) {
702
- if (dangerous.has(key)) return true;
703
+ if (DANGEROUS_KEYS.has(key)) return true;
703
704
  if (visit(node[key], depth + 1)) return true;
704
705
  }
705
706
  return false;
@@ -16,6 +16,7 @@
16
16
 
17
17
  const fs = require('node:fs');
18
18
  const path = require('node:path');
19
+ const { DANGEROUS_KEYS } = require('./flow-io');
19
20
  const {
20
21
  PROJECT_ROOT,
21
22
  parseFlags,
@@ -341,7 +342,7 @@ function applyTemplate(template, data) {
341
342
  }
342
343
 
343
344
  // Forbidden keys to prevent prototype pollution (case-insensitive)
344
- const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
345
+ // Consolidated to flow-io canonical (audit dup-002 / wf-9fc4970b).
345
346
 
346
347
  // Simple substitution: {{key}} or {{object.key}}
347
348
  return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
@@ -351,7 +352,7 @@ function applyTemplate(template, data) {
351
352
  for (const key of keys) {
352
353
  // Prevent prototype pollution attacks (case-insensitive check)
353
354
  const keyLower = key.toLowerCase();
354
- if (FORBIDDEN_KEYS.has(keyLower)) return match;
355
+ if (DANGEROUS_KEYS.has(keyLower)) return match;
355
356
  if (value === undefined || value === null) return match;
356
357
  // Only access own properties
357
358
  if (!Object.hasOwn(value, key)) return match;
@@ -38,8 +38,9 @@ const MODEL_TEMPLATE_MAP = {
38
38
  'gemini-2-flash': 'gemini-flash.yaml'
39
39
  };
40
40
 
41
- // Blocked keys for security (prototype pollution prevention)
42
- const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
41
+ // Blocked keys for security (prototype pollution prevention).
42
+ // Consolidated to flow-io canonical (audit dup-002 / wf-9fc4970b).
43
+ const { DANGEROUS_KEYS: BLOCKED_KEYS } = require('./flow-io');
43
44
 
44
45
  // ============================================================
45
46
  // YAML Parser (lightweight, no dependency)
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow — Worker MCP Strip Helper
5
+ *
6
+ * Generates a channel-only MCP config for worker boot. This is the proper
7
+ * fix for the audit-channel-transport-001 regression: Story A originally
8
+ * wrote `{"mcpServers":{}}` (fully empty) for boot speed, which silently
9
+ * stripped the `wogi-workspace-channel` MCP server — leaving manager-side
10
+ * `workspace_send_message` HTTP-POSTs unable to reach the worker.
11
+ *
12
+ * This script reads the worker member-repo's real `.mcp.json`, extracts
13
+ * ONLY the `wogi-workspace-channel` entry, and writes a channel-only
14
+ * config to a destination path. Result:
15
+ * - claude.ai MCP integrations remain stripped (Story A's boot-speed win)
16
+ * - The workspace transport remains active (manager dispatch works)
17
+ *
18
+ * Fallback: if the source `.mcp.json` doesn't define
19
+ * `wogi-workspace-channel` (e.g. the worker isn't a workspace member),
20
+ * the destination is written with `{"mcpServers":{}}` — harmless in
21
+ * non-workspace contexts.
22
+ *
23
+ * Usage:
24
+ * node flow-worker-mcp-strip.js <source-mcp.json> <dest-mcp.json>
25
+ *
26
+ * Programmatic:
27
+ * const { extractChannelOnlyConfig, writeChannelOnlyConfig } =
28
+ * require('./flow-worker-mcp-strip');
29
+ * const cfg = extractChannelOnlyConfig(srcPath);
30
+ * writeChannelOnlyConfig(destPath, cfg);
31
+ *
32
+ * Exit codes:
33
+ * 0 — success (channel-only or empty config written)
34
+ * 1 — write failure (caller should fall back to no-strip)
35
+ */
36
+
37
+ 'use strict';
38
+
39
+ const fs = require('node:fs');
40
+ const path = require('node:path');
41
+
42
+ const CHANNEL_SERVER_NAME = 'wogi-workspace-channel';
43
+
44
+ /**
45
+ * Read the source `.mcp.json` and return the channel-only config object.
46
+ * Never throws; returns the empty-config fallback on any failure.
47
+ */
48
+ function extractChannelOnlyConfig(sourcePath) {
49
+ const empty = { mcpServers: {} };
50
+ if (!sourcePath || typeof sourcePath !== 'string') return empty;
51
+ try {
52
+ if (!fs.existsSync(sourcePath)) return empty;
53
+ const raw = fs.readFileSync(sourcePath, 'utf-8');
54
+ const parsed = JSON.parse(raw);
55
+ if (!parsed || typeof parsed !== 'object' || !parsed.mcpServers) return empty;
56
+ const entry = parsed.mcpServers[CHANNEL_SERVER_NAME];
57
+ if (!entry || typeof entry !== 'object') return empty;
58
+ return { mcpServers: { [CHANNEL_SERVER_NAME]: entry } };
59
+ } catch (_err) {
60
+ return empty;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Atomically write the channel-only config to destPath. Returns true on
66
+ * success, false on failure (caller should fall back).
67
+ */
68
+ function writeChannelOnlyConfig(destPath, config) {
69
+ if (!destPath || typeof destPath !== 'string') return false;
70
+ try {
71
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
72
+ const tmp = `${destPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
73
+ fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n');
74
+ fs.renameSync(tmp, destPath);
75
+ return true;
76
+ } catch (_err) {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Whether the resulting config preserves the channel transport (i.e. the
83
+ * worker will be reachable from the manager). Useful for callers that want
84
+ * to log a warning if dispatch will silently fail.
85
+ */
86
+ function preservesChannelTransport(config) {
87
+ return Boolean(
88
+ config &&
89
+ config.mcpServers &&
90
+ config.mcpServers[CHANNEL_SERVER_NAME] &&
91
+ typeof config.mcpServers[CHANNEL_SERVER_NAME] === 'object'
92
+ );
93
+ }
94
+
95
+ module.exports = {
96
+ CHANNEL_SERVER_NAME,
97
+ extractChannelOnlyConfig,
98
+ writeChannelOnlyConfig,
99
+ preservesChannelTransport
100
+ };
101
+
102
+ if (require.main === module) {
103
+ const [src, dest] = process.argv.slice(2);
104
+ if (!src || !dest) {
105
+ process.stderr.write('Usage: flow-worker-mcp-strip <source-mcp.json> <dest-mcp.json>\n');
106
+ process.exit(1);
107
+ }
108
+ const cfg = extractChannelOnlyConfig(src);
109
+ const ok = writeChannelOnlyConfig(dest, cfg);
110
+ if (!ok) {
111
+ process.stderr.write(`[flow-worker-mcp-strip] failed to write ${dest}\n`);
112
+ process.exit(1);
113
+ }
114
+ if (!preservesChannelTransport(cfg)) {
115
+ process.stderr.write(
116
+ `[flow-worker-mcp-strip] WARNING: ${src} did not define ${CHANNEL_SERVER_NAME} — ` +
117
+ `worker will boot but manager dispatch will fail. ` +
118
+ `Run "flow workspace init" in the workspace root to regenerate .mcp.json.\n`
119
+ );
120
+ }
121
+ process.exit(0);
122
+ }