mindforge-cc 11.0.0 โ†’ 11.2.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.
Files changed (70) hide show
  1. package/.agent/hooks/mindforge-statusline.js +2 -2
  2. package/.mindforge/config.json +14 -4
  3. package/CHANGELOG.md +137 -0
  4. package/MINDFORGE.md +5 -5
  5. package/RELEASENOTES.md +1 -1
  6. package/bin/autonomous/audit-writer.js +108 -86
  7. package/bin/autonomous/auto-runner.js +304 -19
  8. package/bin/autonomous/dependency-dag.js +59 -0
  9. package/bin/autonomous/mesh-self-healer.js +101 -28
  10. package/bin/autonomous/wave-executor.js +20 -1
  11. package/bin/browser/regression-writer.js +45 -3
  12. package/bin/browser/session-manager.js +21 -17
  13. package/bin/council-cli.js +161 -0
  14. package/bin/dashboard/approval-handler.js +3 -1
  15. package/bin/dashboard/server.js +1 -1
  16. package/bin/dashboard/sse-bridge.js +9 -12
  17. package/bin/engine/council-runtime.js +124 -0
  18. package/bin/engine/logic-drift-detector.js +14 -6
  19. package/bin/engine/logic-validator.js +155 -25
  20. package/bin/engine/orbital-guardian.js +56 -10
  21. package/bin/engine/otel-exporter.js +123 -0
  22. package/bin/engine/reason-source-aligner.js +19 -6
  23. package/bin/engine/remediation-engine.js +1 -1
  24. package/bin/engine/self-corrective-synthesizer.js +1 -1
  25. package/bin/engine/sre-manager.js +33 -6
  26. package/bin/engine/temporal-cli.js +4 -2
  27. package/bin/engine/verification-runner.js +131 -0
  28. package/bin/engine/verify-cli.js +34 -0
  29. package/bin/eval/eval-harness.js +82 -0
  30. package/bin/eval/golden-set-retrieval.json +46 -0
  31. package/bin/governance/audit-hash.js +12 -0
  32. package/bin/governance/audit-verifier.js +60 -0
  33. package/bin/governance/policy-engine.js +17 -4
  34. package/bin/governance/quantum-crypto.js +63 -9
  35. package/bin/governance/ztai-archiver.js +74 -9
  36. package/bin/governance/ztai-manager.js +33 -5
  37. package/bin/hindsight-injector.js +5 -6
  38. package/bin/hooks/instinct-capture-hook.js +186 -0
  39. package/bin/installer-core.js +31 -2
  40. package/bin/memory/auto-shadow.js +32 -3
  41. package/bin/memory/eis-client.js +45 -4
  42. package/bin/memory/identity-synthesizer.js +2 -2
  43. package/bin/memory/knowledge-store.js +30 -6
  44. package/bin/memory/retrieval-fusion.js +58 -0
  45. package/bin/memory/semantic-hub.js +2 -2
  46. package/bin/memory/vector-hub.js +143 -6
  47. package/bin/mindforge-cli.js +4 -5
  48. package/bin/models/anthropic-provider.js +13 -4
  49. package/bin/models/cost-tracker.js +3 -1
  50. package/bin/models/difficulty-scorer.js +54 -0
  51. package/bin/models/gemini-provider.js +6 -2
  52. package/bin/models/model-router.js +31 -18
  53. package/bin/models/openai-provider.js +6 -3
  54. package/bin/models/pricing-registry.js +128 -0
  55. package/bin/review/ads-engine.js +1 -1
  56. package/bin/review/finding-synthesizer.js +35 -6
  57. package/bin/security/trust-boundaries.js +194 -0
  58. package/bin/security/trust-gate-hook.js +49 -0
  59. package/bin/skill-registry.js +34 -22
  60. package/bin/skills-builder/marketplace-cli.js +5 -3
  61. package/bin/skills-builder/skill-registrar.js +4 -6
  62. package/bin/sre/sentinel.js +7 -5
  63. package/bin/sre/shadow-mirror.js +90 -40
  64. package/bin/utils/append-queue.js +67 -0
  65. package/bin/utils/file-io.js +29 -80
  66. package/bin/utils/version-check.js +75 -0
  67. package/bin/verify-audit.js +12 -0
  68. package/bin/wizard/theme.js +1 -2
  69. package/package.json +1 -1
  70. package/bin/dashboard/team-tracker.js +0 -0
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ /**
6
+ * Recursively sorts object keys for deterministic JSON serialization.
7
+ * Arrays are preserved in order; nested objects get sorted keys.
8
+ */
9
+ function stableStringify(value) {
10
+ if (value === null || typeof value !== 'object') {
11
+ return JSON.stringify(value);
12
+ }
13
+ if (Array.isArray(value)) {
14
+ return '[' + value.map(item => stableStringify(item)).join(',') + ']';
15
+ }
16
+ const sortedKeys = Object.keys(value).sort();
17
+ const pairs = sortedKeys.map(key => {
18
+ return JSON.stringify(key) + ':' + stableStringify(value[key]);
19
+ });
20
+ return '{' + pairs.join(',') + '}';
21
+ }
22
+
23
+ /**
24
+ * Computes SHA-256 hash of a manifest using stable key-sorted serialization.
25
+ * Returns { name, hash, pinnedAt }.
26
+ */
27
+ function pinManifest(manifest) {
28
+ const serialized = stableStringify(manifest);
29
+ const hash = crypto.createHash('sha256').update(serialized).digest('hex');
30
+ return {
31
+ name: manifest.name,
32
+ hash,
33
+ pinnedAt: Date.now()
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Verifies a manifest against a previously pinned hash.
39
+ * Returns { valid: true } or { valid: false, reason }.
40
+ */
41
+ function verifyManifest(manifest, pin) {
42
+ const serialized = stableStringify(manifest);
43
+ const computed = crypto.createHash('sha256').update(serialized).digest('hex');
44
+ if (computed === pin.hash) {
45
+ return { valid: true };
46
+ }
47
+ return {
48
+ valid: false,
49
+ reason: `hash mismatch: expected ${pin.hash}, got ${computed}`
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Wraps content with untrusted provenance metadata.
55
+ * Returns { content, trusted: false, provenance: { source, tool, timestamp } }.
56
+ */
57
+ function tagUntrusted(content, meta) {
58
+ return {
59
+ content,
60
+ trusted: false,
61
+ provenance: {
62
+ source: meta.source,
63
+ tool: meta.tool,
64
+ timestamp: Date.now()
65
+ }
66
+ };
67
+ }
68
+
69
+ // Null byte (char code 0). Built via fromCharCode so we never embed a control
70
+ // character in a regex literal (eslint no-control-regex).
71
+ const NUL = String.fromCharCode(0);
72
+
73
+ // ${IFS} / $IFS are shell-internal field separators that expand to whitespace.
74
+ // Attackers use them to avoid literal spaces between a destructive token and
75
+ // its flags (rm${IFS}-rf${IFS}/). We normalize them back to a space so the
76
+ // existing rm/-rf patterns catch the de-obfuscated form (audit #8).
77
+ const IFS_TOKEN = /\$\{IFS\}|\$IFS/g;
78
+
79
+ /**
80
+ * De-obfuscates shell metacharacter tricks WITHOUT emulating a real shell.
81
+ * Strips quotes (' ") and backslash escapes, collapses ${IFS}/$IFS to a space,
82
+ * then collapses runs of whitespace. This turns r''m, r"m, r\m and
83
+ * rm${IFS}-rf${IFS}/ back into plain `rm -rf /` so the existing patterns fire.
84
+ * Intentionally conservative: it removes characters rather than interpreting
85
+ * them, which can only make a string MORE likely to match (fail-toward-block).
86
+ */
87
+ function normalizeShell(input) {
88
+ return input
89
+ .split(NUL).join('') // shells ignore NUL; never let it split a token
90
+ .replace(IFS_TOKEN, ' ') // ${IFS}/$IFS -> space
91
+ .replace(/[\\'"]/g, '') // drop backslash escapes and quote chars
92
+ .replace(/\s+/g, ' '); // collapse whitespace runs
93
+ }
94
+
95
+ /**
96
+ * Detects high-impact / destructive commands via case-insensitive pattern matching.
97
+ * Returns true if the command matches known destructive patterns.
98
+ *
99
+ * The detector errs toward blocking by design: this feeds a PreToolUse gate
100
+ * where approval friction is strictly preferable to a destructive bypass.
101
+ */
102
+ function isHighImpact(command) {
103
+ // Normalize first so metacharacter-obfuscated commands (audit #8) are matched
104
+ // by the SAME pattern set as their plain-text equivalents.
105
+ const sanitized = normalizeShell(String(command));
106
+ const patterns = [
107
+ // โ”€โ”€ Original allowlist (unchanged) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
108
+ /rm\s+(-\w*r\w*\s+-\w*f|(-\w*f\w*\s+-\w*r)|-\w*rf|-\w*fr)/i,
109
+ /git\s+push\s+.*--force/i,
110
+ /git\s+push\s+.*-f/i,
111
+ /drop\s+(table|database)/i,
112
+ /git\s+reset\s+--hard/i,
113
+ /delete\s+from/i,
114
+ /truncate\s+table/i,
115
+ /\bmkfs(\.\w+)?\s+\/dev\//i,
116
+ // #11: any dd write target, not just /dev/ (dd if=... of=important.db).
117
+ // Original /dev/-only check is a subset of this, so it stays covered.
118
+ /\bdd\b.*\bof=/i,
119
+ /\b(curl|wget)\b.*\|\s*(bash|sh|zsh)\b/i,
120
+ /^\s*find\s+.*-delete\b/i,
121
+
122
+ // โ”€โ”€ #4 Command/process substitution RCE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
123
+ // `eval` anywhere is high-impact (dynamic code execution).
124
+ /\beval\b/i,
125
+ // Command substitution $(...) or backtick combined with a network fetch.
126
+ /\$\(\s*(curl|wget)\b/i,
127
+ new RegExp(String.fromCharCode(96) + '\\s*(curl|wget)\\b', 'i'),
128
+ // Process substitution feeding an interpreter: bash <(...), sh <(...).
129
+ /\b(bash|sh|zsh)\b.*<\(/i,
130
+ // Curl/wget directly inside a process substitution.
131
+ /<\(\s*(curl|wget)\b/i,
132
+
133
+ // โ”€โ”€ #5 Interpreter invocation of a script file โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
134
+ // source <file> and `. <file>` are unambiguous script execution.
135
+ /\bsource\s+\S+/i,
136
+ /(^|[;&|]|\s)\.\s+\S+\.\w+/i,
137
+ // bash/sh/zsh running a .sh script โ€” clearly script execution. Kept broad
138
+ // (any .sh) because shelling out to an arbitrary shell script is itself a
139
+ // strong execution signal; this also covers untrusted paths like
140
+ // `bash /tmp/x.sh`.
141
+ /\b(bash|sh|zsh)\s+\S*\.sh\b/i,
142
+ // node/python/etc. running a script โ€” narrowed (UC-22). Running a project
143
+ // file is THE default safe action in a Node/Python repo, so a blanket
144
+ // `node <file>.js` match is a terrible signal-to-noise ratio and blocked
145
+ // the project's OWN idiom (`node tests/run-all.js`). We now flag ONLY when
146
+ // the script path looks UNTRUSTED โ€” an absolute path (/...), a writable
147
+ // temp dir (/tmp, /var/tmp, /dev/shm), or a home-relative path (~/...) โ€”
148
+ // i.e. the write-then-execute attack chain (drop payload in a writable
149
+ // location, then run it). Project-relative paths (tests/run-all.js,
150
+ // bin/foo.js, scripts/build.py, index.js) are NOT matched. Piped/
151
+ // substituted execution (curl | bash, $(...), <(...), backticks) is
152
+ // already covered by the #4 patterns above.
153
+ /\b(node|python3?|ruby|perl)\s+(~\/|\/)\S*\.(js|mjs|cjs|ts|py|rb|pl)\b/i,
154
+
155
+ // โ”€โ”€ #7 Redirect-overwrite of critical files / devices โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
156
+ // > or >> targeting an absolute sensitive path (/etc/, /dev/, /boot/, /sys/,
157
+ // /proc/, /var/, /usr/, /bin/, /sbin/, /lib/). Project-local redirects
158
+ // (> out.log) are NOT matched.
159
+ />>?\s*\/(etc|dev|boot|sys|proc|var|usr|bin|sbin|lib|root|lib64)\//i,
160
+
161
+ // โ”€โ”€ #8 handled by normalizeShell() above (de-obfuscation), no pattern here.
162
+
163
+ // โ”€โ”€ #9 chmod dangerous modes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
164
+ // Canonical dangerous octal modes only: 000 (full lockout) and the
165
+ // world-writable 666/777 family. Common safe modes (644/755/600/700/640)
166
+ // and symbolic modes (chmod +x build.sh) are intentionally NOT matched.
167
+ // ANY recursive chmod is also treated as high-impact regardless of mode.
168
+ /\bchmod\b.*\b(000|0?666|0?777)\b/i,
169
+ /\bchmod\s+-\w*[rR]/i,
170
+
171
+ // โ”€โ”€ #10 chown recursive โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
172
+ /\bchown\s+-\w*[rR]/i,
173
+
174
+ // โ”€โ”€ #12 mv of root / into /dev/null โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
175
+ /\bmv\b.*\/dev\/null\b/i,
176
+ /\bmv\s+(-\w+\s+)?\/\s/i,
177
+
178
+ // โ”€โ”€ #13 Process killers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
179
+ /\bkill\s+-9\s+-1\b/i,
180
+ /\bkillall\b/i,
181
+ /\bpkill\b/i,
182
+
183
+ // โ”€โ”€ #14 Power-state commands โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
184
+ /\b(shutdown|reboot|halt|poweroff)\b/i,
185
+ ];
186
+ return patterns.some(pattern => pattern.test(sanitized));
187
+ }
188
+
189
+ module.exports = {
190
+ pinManifest,
191
+ verifyManifest,
192
+ tagUntrusted,
193
+ isHighImpact
194
+ };
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { isHighImpact } = require('./trust-boundaries');
5
+
6
+ let input = '';
7
+ process.stdin.setEncoding('utf8');
8
+ process.stdin.on('data', (chunk) => { input += chunk; });
9
+ process.stdin.on('end', () => {
10
+ try {
11
+ const event = JSON.parse(input);
12
+
13
+ // Only gate Bash tool calls
14
+ if (event.tool_name !== 'Bash') {
15
+ process.exit(0); // allow
16
+ }
17
+
18
+ const fullCommand = event.tool_input?.command || '';
19
+
20
+ // Check the whole command AND every individual line, blocking if ANY
21
+ // segment is high-impact. Per-line scanning means a benign first line
22
+ // cannot cloak a destructive command on a later line; the whole-string
23
+ // check catches patterns a line split might fragment. This is a security
24
+ // gate, so it errs toward blocking: a destructive keyword in (e.g.) a
25
+ // commit message will prompt for approval rather than risk a cloaked
26
+ // command slipping through. Approval friction is preferable to a bypass.
27
+ const lines = fullCommand.split('\n');
28
+ const offending = [fullCommand, ...lines].find((segment) => isHighImpact(segment));
29
+
30
+ if (offending) {
31
+ const display = offending.length > 80 ? offending.slice(0, 80) + '...' : offending;
32
+ // Output a block reason (Claude Code shows this to the user)
33
+ process.stdout.write(JSON.stringify({
34
+ decision: 'block',
35
+ reason: `[TrustGate] High-impact command detected: "${display}" โ€” requires explicit user approval`
36
+ }));
37
+ process.exit(2); // block
38
+ }
39
+
40
+ process.exit(0); // allow
41
+ } catch (e) {
42
+ process.stderr.write('[trust-gate-hook] parse error (BLOCKING): ' + e.message + '\n');
43
+ process.stdout.write(JSON.stringify({
44
+ decision: 'block',
45
+ reason: '[TrustGate] Could not verify command safety โ€” parse error'
46
+ }));
47
+ process.exit(2);
48
+ }
49
+ });
@@ -124,31 +124,42 @@ function handleInstall() {
124
124
  if (tier === '1') targetBase = '.mindforge/skills';
125
125
  if (tier === '3') targetBase = '.mindforge/project-skills';
126
126
 
127
+ // Resolve a real local source for the skill. There is no remote registry
128
+ // backend, so the ONLY honest sources are an examples/ entry or a SKILL.md in
129
+ // the current working directory (the agentic authoring flow).
130
+ const exampleSrc = path.join(process.cwd(), 'examples', 'skills', `${skillName}.md`);
131
+ const localSrc = path.join(process.cwd(), 'SKILL.md');
132
+
133
+ let source = null;
134
+ if (fs.existsSync(exampleSrc)) {
135
+ source = exampleSrc;
136
+ } else if (fs.existsSync(localSrc)) {
137
+ source = localSrc;
138
+ }
139
+
140
+ if (!source) {
141
+ // HONEST REFUSAL (UC-22): no local source and no remote registry is
142
+ // configured. Do NOT write a placeholder file that masquerades as an
143
+ // installed skill โ€” that would falsely report success. Refuse clearly and
144
+ // exit non-zero so callers can detect the failure.
145
+ console.error(
146
+ `โŒ Skill '${skillName}' not found locally and no remote registry is configured โ€” nothing installed.`
147
+ );
148
+ console.error(
149
+ ' Provide a source first: place a SKILL.md in the current directory ' +
150
+ `or add examples/skills/${skillName}.md, then re-run.`
151
+ );
152
+ process.exit(1);
153
+ }
154
+
155
+ // Real source found โ†’ perform the actual install.
127
156
  const targetDir = path.join(process.cwd(), targetBase, skillName);
128
157
  if (!fs.existsSync(targetDir)) {
129
158
  fs.mkdirSync(targetDir, { recursive: true });
130
159
  }
131
-
132
- // Mock behavior for Day 3/4 testing if no real registry
133
- // Expect source to be in examples/ if it exists, otherwise just touch it
134
- const mockSrc = path.join(process.cwd(), 'examples', 'skills', `${skillName}.md`);
135
160
  const targetFile = path.join(targetDir, 'SKILL.md');
136
-
137
- if (fs.existsSync(mockSrc)) {
138
- fs.copyFileSync(mockSrc, targetFile);
139
- console.log(` โœ… Copied from ${mockSrc}`);
140
- } else {
141
- // If no source, we look for it in the current dir for the agentic flow
142
- const localSrc = path.join(process.cwd(), 'SKILL.md');
143
- if (fs.existsSync(localSrc)) {
144
- fs.copyFileSync(localSrc, targetFile);
145
- console.log(' โœ… Copied from local SKILL.md');
146
- } else {
147
- // Just create a placeholder for testing if nothing else
148
- fs.writeFileSync(targetFile, `---\nname: ${skillName}\nversion: 1.0.0\nstatus: alpha\ntriggers: test, mock, placeholder\n---\n# ${skillName}\nPlaceholder for ${skillName} skill.\n`);
149
- console.log(' โš ๏ธ Created mock SKILL.md (no source found)');
150
- }
151
- }
161
+ fs.copyFileSync(source, targetFile);
162
+ console.log(` โœ… Installed ${skillName} from ${path.relative(process.cwd(), source)}`);
152
163
 
153
164
  process.exit(0);
154
165
  }
@@ -215,7 +226,6 @@ function handleAudit() {
215
226
  }
216
227
 
217
228
  const entry = {
218
- timestamp: new Date().toISOString(),
219
229
  event: 'skill_installed',
220
230
  skill_name: skillName,
221
231
  skill_version: version,
@@ -224,7 +234,9 @@ function handleAudit() {
224
234
  validation_passed: true
225
235
  };
226
236
 
227
- fs.appendFileSync(auditPath, JSON.stringify(entry) + '\n', 'utf8');
237
+ // UC-04b: unified, hash-chained, durable append into the single verifiable chain.
238
+ const { appendAuditEntrySync } = require('./autonomous/audit-writer');
239
+ appendAuditEntrySync(auditPath, entry);
228
240
  console.log(` ๐Ÿ“ Audit entry written for ${skillName}`);
229
241
  process.exit(0);
230
242
  }
@@ -19,7 +19,7 @@ if (!CMD) {
19
19
  async function main() {
20
20
  try {
21
21
  switch (CMD) {
22
- case 'search':
22
+ case 'search': {
23
23
  const results = await Marketplace.search(QUERY);
24
24
  console.table(results.map(r => ({
25
25
  name: r.name,
@@ -28,12 +28,14 @@ async function main() {
28
28
  description: r.description.slice(0, 50) + '...'
29
29
  })));
30
30
  break;
31
-
31
+ }
32
+
32
33
  case 'featured':
33
- case 'trending':
34
+ case 'trending': {
34
35
  const list = await Marketplace.getFeatured();
35
36
  console.table(list);
36
37
  break;
38
+ }
37
39
 
38
40
  case 'install':
39
41
  if (!QUERY) throw new Error('Package name required for install');
@@ -82,11 +82,10 @@ function register(params) {
82
82
  );
83
83
  }
84
84
 
85
- // Write AUDIT entry
85
+ // Write AUDIT entry via the unified, hash-chained, durable append (UC-04b).
86
86
  if (fs.existsSync(path.dirname(AUDIT_PATH))) {
87
- const entry = {
88
- id: require('crypto').randomBytes(8).toString('hex'),
89
- timestamp: new Date().toISOString(),
87
+ const { appendAuditEntrySync } = require('../autonomous/audit-writer');
88
+ appendAuditEntrySync(AUDIT_PATH, {
90
89
  event: 'skill_learned',
91
90
  agent: 'mindforge-skills-builder',
92
91
  phase: null,
@@ -97,8 +96,7 @@ function register(params) {
97
96
  source_type: sourceType,
98
97
  source: String(source).slice(0, 200),
99
98
  skill_path: relativePath,
100
- };
101
- fs.appendFileSync(AUDIT_PATH, JSON.stringify(entry) + '\n');
99
+ });
102
100
  }
103
101
 
104
102
  return { registered: true, skillName, tier, qualityScore };
@@ -109,13 +109,15 @@ class Sentinel {
109
109
  }
110
110
 
111
111
  logToAudit(event, targetPath) {
112
- const logEntry = {
113
- id: crypto.randomBytes(8).toString('hex'),
114
- timestamp: new Date().toISOString(),
112
+ // UC-04b: route through the unified, hash-chained, durable append so Sentinel's
113
+ // incident entries link into the single verifiable chain (was a raw appendFileSync
114
+ // that broke the chain). appendAuditEntrySync caches the chain head per resolved
115
+ // path, so an explicit targetPath is chained correctly too.
116
+ const { appendAuditEntrySync } = require('../autonomous/audit-writer');
117
+ appendAuditEntrySync(targetPath || this.auditPath, {
115
118
  agent: 'mindforge-sentinel',
116
119
  ...event
117
- };
118
- fs.appendFileSync(targetPath || this.auditPath, JSON.stringify(logEntry) + '\n');
120
+ });
119
121
  }
120
122
 
121
123
  stop() {
@@ -1,13 +1,28 @@
1
1
  /**
2
2
  * MindForge v9.0.0 โ€” Temporal Shadow Mirror (Pillar XXI)
3
- * Hybrid isolation for incident replication.
3
+ *
4
+ * Incident replication via git-worktree isolation.
5
+ *
6
+ * ISOLATION HONESTY (audit finding #24 / UC-22):
7
+ * This module provides GIT WORKTREE isolation only โ€” a separate working tree
8
+ * and branch off the current repo. It does NOT provide container/Docker
9
+ * isolation (no separate kernel, network, filesystem, or process namespaces).
10
+ * A `docker --version` probe gates an "enhanced" path, but that path still runs
11
+ * in a git worktree; real Docker orchestration (build/run/mount) is a
12
+ * NOT-YET-IMPLEMENTED future enhancement. The returned mirror is always a
13
+ * worktree, and `activeMirror.isolation` reports the actual level provided.
4
14
  */
5
15
  'use strict';
6
16
 
7
- const { execSync } = require('node:child_process');
17
+ const { execSync, execFileSync } = require('node:child_process');
8
18
  const fs = require('node:fs');
9
19
  const path = require('node:path');
10
- const crypto = require('node:crypto');
20
+
21
+ // remediation_id flows from the SRE sentinel (a trust boundary) into git
22
+ // branch/worktree names. Only allow a safe identifier charset and reject
23
+ // anything that could be read as a flag or shell metacharacter. Defence in
24
+ // depth on top of execFileSync (argv array, no shell). Fail-closed.
25
+ const SAFE_REMEDIATION_ID = /^[A-Za-z0-9._-]+$/;
11
26
 
12
27
  class ShadowMirror {
13
28
  constructor(options = {}) {
@@ -15,32 +30,58 @@ class ShadowMirror {
15
30
  this.activeMirror = null;
16
31
  }
17
32
 
33
+ /**
34
+ * Validates a remediation_id (trust-boundary input) before it is used to
35
+ * build git branch/worktree names. Fail-closed: throws on missing, empty,
36
+ * overlong, flag-like, or metacharacter-bearing ids. Returns the id on pass.
37
+ * Static so it can be unit-tested in isolation.
38
+ */
39
+ static sanitizeRemediationId(id) {
40
+ if (typeof id !== 'string' || id.length === 0 || id.length > 128) {
41
+ throw new Error('[ShadowMirror] invalid remediation_id: must be a non-empty string of <=128 chars');
42
+ }
43
+ if (id.startsWith('-')) {
44
+ throw new Error('[ShadowMirror] invalid remediation_id: must not start with "-"');
45
+ }
46
+ if (!SAFE_REMEDIATION_ID.test(id)) {
47
+ throw new Error('[ShadowMirror] invalid remediation_id: only [A-Za-z0-9._-] characters allowed');
48
+ }
49
+ return id;
50
+ }
51
+
18
52
  /**
19
53
  * Orchestrates replication based on incident severity and requirements.
20
54
  */
21
55
  async replicate(incident) {
22
56
  console.log(`๐ŸŒ€ Shadow Mirror: Replicating incident [${incident.remediation_id}]...`);
23
-
24
- // Choose isolation level
25
- const level = (incident.details?.severity === 'CRITICAL') ? 2 : 1;
26
-
27
- if (level === 2 && this.isDockerAvailable()) {
28
- return this.replicateLevel2(incident);
29
- } else {
30
- return this.replicateLevel1(incident);
57
+
58
+ // All replication currently runs in a git worktree (see file header).
59
+ // For CRITICAL incidents where a Docker toolchain is present we note that a
60
+ // future container-isolation path could be used, but we DO NOT claim to run
61
+ // it โ€” we still replicate in a worktree and label it honestly.
62
+ const wantsContainer = incident.details?.severity === 'CRITICAL' && this.isDockerAvailable();
63
+
64
+ if (wantsContainer) {
65
+ return this.replicateCriticalWorktree(incident);
31
66
  }
67
+ return this.replicateLevel1(incident);
32
68
  }
33
69
 
34
70
  /**
35
71
  * Level 1 Replication: Git Worktree
36
- * High-speed, lightweight logic isolation.
72
+ * High-speed, lightweight logic isolation (NOT container isolation).
73
+ *
74
+ * Provides a separate working tree + branch off the current repo. This
75
+ * isolates source/logic changes; it shares the host kernel, network,
76
+ * filesystem and process namespaces with the running engine.
37
77
  */
38
78
  async replicateLevel1(incident) {
39
- const mirrorId = `mirror-${incident.remediation_id}`;
79
+ const remediationId = ShadowMirror.sanitizeRemediationId(incident.remediation_id);
80
+ const mirrorId = `mirror-${remediationId}`;
40
81
  const mirrorPath = path.join(this.baseDir, mirrorId);
41
- const branchName = `sre-repro-${incident.remediation_id}`;
82
+ const branchName = `sre-repro-${remediationId}`;
42
83
 
43
- console.log(`[Level 1] Creating git worktree at ${mirrorPath}...`);
84
+ console.log(`[worktree] Creating git worktree at ${mirrorPath}...`);
44
85
 
45
86
  try {
46
87
  if (!fs.existsSync(this.baseDir)) {
@@ -49,41 +90,46 @@ class ShadowMirror {
49
90
 
50
91
  // 1. Create a reproduction branch and add worktree in one step (Atomic)
51
92
  // In a real system, we'd base this on the commit hash from the trace_id
52
- execSync(`git worktree add -b ${branchName} ${mirrorPath}`, { stdio: 'ignore' });
53
-
54
- this.activeMirror = { path: mirrorPath, branch: branchName, type: 'worktree' };
93
+ // execFileSync with an argv array โ€” no shell, so branchName/mirrorPath
94
+ // cannot inject commands even if they somehow contained metacharacters.
95
+ execFileSync('git', ['worktree', 'add', '-b', branchName, mirrorPath], { stdio: 'ignore' });
96
+
97
+ this.activeMirror = {
98
+ path: mirrorPath,
99
+ branch: branchName,
100
+ type: 'worktree-isolation',
101
+ // Honest disclosure of the isolation actually provided.
102
+ isolation: 'git-worktree',
103
+ };
55
104
 
56
105
  // 3. Inject incident metadata for the agent to use
57
106
  fs.writeFileSync(path.join(mirrorPath, 'REPLICATION.json'), JSON.stringify(incident, null, 2));
58
107
 
59
108
  return mirrorPath;
60
109
  } catch (err) {
61
- console.error(`[ShadowMirror] Level 1 replication failed: ${err.message}`);
110
+ console.error(`[ShadowMirror] worktree replication failed: ${err.message}`);
62
111
  throw err;
63
112
  }
64
113
  }
65
114
 
66
115
  /**
67
- * Level 2 Replication: Docker Sandbox
68
- * Full environment isolation for state-bound incidents.
116
+ * CRITICAL-incident replication when a Docker toolchain is detected.
117
+ *
118
+ * HONESTY: This still replicates in a GIT WORKTREE. It does not build or run
119
+ * a container, does not mount any volume, and does not provide container
120
+ * isolation. Real Docker sandboxing (build/run/mount) is a NOT-YET-IMPLEMENTED
121
+ * enhancement tracked separately; until it lands, this path is identical in
122
+ * isolation guarantees to {@link replicateLevel1} and is labelled as such.
69
123
  */
70
- async replicateLevel2(incident) {
71
- console.log('[Level 2] Initializing Docker sandbox interface...');
72
- // For the hackathon demo, we'll scaffold the Docker-ready worktree which would then be mounted
124
+ async replicateCriticalWorktree(incident) {
125
+ console.log('[worktree] CRITICAL incident: replicating in git worktree (container isolation not implemented).');
73
126
  const mirrorPath = await this.replicateLevel1(incident);
74
-
75
- const dockerfile = `
76
- FROM node:18-slim
77
- WORKDIR /app
78
- COPY . .
79
- RUN npm install --production
80
- CMD ["node", "bin/engine/logic-validator.js"]
81
- `;
82
-
83
- fs.writeFileSync(path.join(mirrorPath, 'Dockerfile.sre'), dockerfile);
84
- console.log(`[Level 2] Dockerfile.sre generated at ${mirrorPath}. Mounting volume for isolation.`);
85
-
86
- this.activeMirror.type = 'docker-hybrid';
127
+
128
+ // Note the unimplemented future path without claiming it ran.
129
+ this.activeMirror.dockerAvailable = true;
130
+ this.activeMirror.note =
131
+ 'docker binary detected; real container isolation is a future enhancement โ€” this mirror used git-worktree isolation only';
132
+
87
133
  return mirrorPath;
88
134
  }
89
135
 
@@ -104,9 +150,13 @@ CMD ["node", "bin/engine/logic-validator.js"]
104
150
 
105
151
  console.log(`๐Ÿงน Cleaning up Shadow Mirror: ${this.activeMirror.path}...`);
106
152
  try {
107
- if (this.activeMirror.type === 'worktree' || this.activeMirror.type === 'docker-hybrid') {
108
- execSync(`git worktree remove ${this.activeMirror.path} --force`, { stdio: 'ignore' });
109
- execSync(`git branch -D ${this.activeMirror.branch}`, { stdio: 'ignore' });
153
+ // Every mirror this module produces is a git worktree (see replicate()).
154
+ // Accept the legacy 'worktree'/'docker-hybrid' labels for backward compat
155
+ // in case an older serialized mirror is handed in.
156
+ const worktreeTypes = ['worktree-isolation', 'worktree', 'docker-hybrid'];
157
+ if (worktreeTypes.includes(this.activeMirror.type)) {
158
+ execFileSync('git', ['worktree', 'remove', this.activeMirror.path, '--force'], { stdio: 'ignore' });
159
+ execFileSync('git', ['branch', '-D', this.activeMirror.branch], { stdio: 'ignore' });
110
160
  // Clean up the folder just in case
111
161
  if (fs.existsSync(this.activeMirror.path)) {
112
162
  fs.rmSync(this.activeMirror.path, { recursive: true });
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+ /**
3
+ * MindForge โ€” Single-writer serialized append queue (UC-09).
4
+ * Guarantees: (1) appends to a given file are serialized (no interleaving across
5
+ * concurrent callers in this process), (2) each append() resolves only after the
6
+ * bytes are fsync'd to disk (durability), (3) a trailing newline delimits records.
7
+ *
8
+ * Scope: protects against in-process concurrent-write interleaving and crash-loss
9
+ * of acknowledged writes. Cross-PROCESS locking is out of scope for the
10
+ * single-operator localhost model (documented; revisit if multi-process writers appear).
11
+ */
12
+ const fs = require('fs');
13
+
14
+ // path -> Promise chain tail. NOTE: this is intended for a small, fixed set of
15
+ // known paths (e.g. AUDIT.jsonl). Per-path entries are NEVER evicted, so do NOT
16
+ // key this by high-cardinality dynamic paths โ€” doing so would leak memory
17
+ // unboundedly. Revisit with an LRU/eviction policy if dynamic paths are needed.
18
+ const queues = new Map();
19
+
20
+ /**
21
+ * @deprecated No production caller. The real durable-append path is
22
+ * `appendDurableSync` in `bin/memory/knowledge-store.js` (synchronous
23
+ * openSync+writeSync+fsyncSync+closeSync), which all audit/KB write sites use.
24
+ * This async single-writer queue is retained only as a tested reference
25
+ * implementation for the in-process serialized-append pattern (UC-09) and is
26
+ * exercised solely by `tests/append-queue.test.js`. Do NOT wire it into new
27
+ * code without first reconciling it with the canonical sync path.
28
+ *
29
+ * @param {string} filePath โ€” file to serialize appends for
30
+ * @returns {{append: (line: string) => Promise<void>, drain: () => Promise<void>}}
31
+ */
32
+ function createAppendQueue(filePath) {
33
+ if (!queues.has(filePath)) queues.set(filePath, Promise.resolve());
34
+
35
+ function append(line) {
36
+ const record = line.endsWith('\n') ? line : line + '\n';
37
+ const tail = queues.get(filePath).then(() => writeDurable(filePath, record));
38
+ queues.set(filePath, tail.catch(() => {}));
39
+ return tail;
40
+ }
41
+
42
+ function drain() {
43
+ return queues.get(filePath);
44
+ }
45
+
46
+ return Object.freeze({ append, drain });
47
+ }
48
+
49
+ function writeDurable(filePath, data) {
50
+ return new Promise((resolve, reject) => {
51
+ fs.open(filePath, 'a', (openErr, fd) => {
52
+ if (openErr) return reject(openErr);
53
+ fs.write(fd, data, (writeErr) => {
54
+ if (writeErr) { fs.close(fd, () => {}); return reject(writeErr); }
55
+ fs.fsync(fd, (syncErr) => {
56
+ fs.close(fd, (closeErr) => {
57
+ if (syncErr) return reject(syncErr);
58
+ if (closeErr) return reject(closeErr);
59
+ resolve();
60
+ });
61
+ });
62
+ });
63
+ });
64
+ });
65
+ }
66
+
67
+ module.exports = { createAppendQueue };