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.
- package/.agent/hooks/mindforge-statusline.js +2 -2
- package/.mindforge/config.json +14 -4
- package/CHANGELOG.md +137 -0
- package/MINDFORGE.md +5 -5
- package/RELEASENOTES.md +1 -1
- package/bin/autonomous/audit-writer.js +108 -86
- package/bin/autonomous/auto-runner.js +304 -19
- package/bin/autonomous/dependency-dag.js +59 -0
- package/bin/autonomous/mesh-self-healer.js +101 -28
- package/bin/autonomous/wave-executor.js +20 -1
- package/bin/browser/regression-writer.js +45 -3
- package/bin/browser/session-manager.js +21 -17
- package/bin/council-cli.js +161 -0
- package/bin/dashboard/approval-handler.js +3 -1
- package/bin/dashboard/server.js +1 -1
- package/bin/dashboard/sse-bridge.js +9 -12
- package/bin/engine/council-runtime.js +124 -0
- package/bin/engine/logic-drift-detector.js +14 -6
- package/bin/engine/logic-validator.js +155 -25
- package/bin/engine/orbital-guardian.js +56 -10
- package/bin/engine/otel-exporter.js +123 -0
- package/bin/engine/reason-source-aligner.js +19 -6
- package/bin/engine/remediation-engine.js +1 -1
- package/bin/engine/self-corrective-synthesizer.js +1 -1
- package/bin/engine/sre-manager.js +33 -6
- package/bin/engine/temporal-cli.js +4 -2
- package/bin/engine/verification-runner.js +131 -0
- package/bin/engine/verify-cli.js +34 -0
- package/bin/eval/eval-harness.js +82 -0
- package/bin/eval/golden-set-retrieval.json +46 -0
- package/bin/governance/audit-hash.js +12 -0
- package/bin/governance/audit-verifier.js +60 -0
- package/bin/governance/policy-engine.js +17 -4
- package/bin/governance/quantum-crypto.js +63 -9
- package/bin/governance/ztai-archiver.js +74 -9
- package/bin/governance/ztai-manager.js +33 -5
- package/bin/hindsight-injector.js +5 -6
- package/bin/hooks/instinct-capture-hook.js +186 -0
- package/bin/installer-core.js +31 -2
- package/bin/memory/auto-shadow.js +32 -3
- package/bin/memory/eis-client.js +45 -4
- package/bin/memory/identity-synthesizer.js +2 -2
- package/bin/memory/knowledge-store.js +30 -6
- package/bin/memory/retrieval-fusion.js +58 -0
- package/bin/memory/semantic-hub.js +2 -2
- package/bin/memory/vector-hub.js +143 -6
- package/bin/mindforge-cli.js +4 -5
- package/bin/models/anthropic-provider.js +13 -4
- package/bin/models/cost-tracker.js +3 -1
- package/bin/models/difficulty-scorer.js +54 -0
- package/bin/models/gemini-provider.js +6 -2
- package/bin/models/model-router.js +31 -18
- package/bin/models/openai-provider.js +6 -3
- package/bin/models/pricing-registry.js +128 -0
- package/bin/review/ads-engine.js +1 -1
- package/bin/review/finding-synthesizer.js +35 -6
- package/bin/security/trust-boundaries.js +194 -0
- package/bin/security/trust-gate-hook.js +49 -0
- package/bin/skill-registry.js +34 -22
- package/bin/skills-builder/marketplace-cli.js +5 -3
- package/bin/skills-builder/skill-registrar.js +4 -6
- package/bin/sre/sentinel.js +7 -5
- package/bin/sre/shadow-mirror.js +90 -40
- package/bin/utils/append-queue.js +67 -0
- package/bin/utils/file-io.js +29 -80
- package/bin/utils/version-check.js +75 -0
- package/bin/verify-audit.js +12 -0
- package/bin/wizard/theme.js +1 -2
- package/package.json +1 -1
- 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
|
+
});
|
package/bin/skill-registry.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
88
|
-
|
|
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 };
|
package/bin/sre/sentinel.js
CHANGED
|
@@ -109,13 +109,15 @@ class Sentinel {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
logToAudit(event, targetPath) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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() {
|
package/bin/sre/shadow-mirror.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MindForge v9.0.0 โ Temporal Shadow Mirror (Pillar XXI)
|
|
3
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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-${
|
|
82
|
+
const branchName = `sre-repro-${remediationId}`;
|
|
42
83
|
|
|
43
|
-
console.log(`[
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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]
|
|
110
|
+
console.error(`[ShadowMirror] worktree replication failed: ${err.message}`);
|
|
62
111
|
throw err;
|
|
63
112
|
}
|
|
64
113
|
}
|
|
65
114
|
|
|
66
115
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
|
71
|
-
console.log('[
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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 };
|