sinapse-ai 1.20.0 → 1.21.0
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/.sinapse-ai/core/atlas/render-html.js +10 -8
- package/.sinapse-ai/core/orchestration/brownfield-progress.js +219 -0
- package/.sinapse-ai/core/orchestration/doc-first-resolver.js +3 -0
- package/.sinapse-ai/core/orchestration/greenfield-handler.js +97 -0
- package/.sinapse-ai/core/orchestration/workflow-executor.js +17 -1
- package/.sinapse-ai/core/registry/service-registry.json +2 -32
- package/.sinapse-ai/data/entity-registry.yaml +473 -203
- package/.sinapse-ai/development/agents/analyst.md +18 -34
- package/.sinapse-ai/development/agents/architect.md +18 -34
- package/.sinapse-ai/development/agents/data-engineer.md +15 -28
- package/.sinapse-ai/development/agents/developer.md +0 -6
- package/.sinapse-ai/development/agents/devops.md +15 -28
- package/.sinapse-ai/development/agents/product-lead.md +18 -34
- package/.sinapse-ai/development/agents/project-lead.md +18 -34
- package/.sinapse-ai/development/agents/quality-gate.md +18 -34
- package/.sinapse-ai/development/agents/squad-creator.md +15 -27
- package/.sinapse-ai/development/agents/ux-design-expert.md +15 -27
- package/.sinapse-ai/development/scripts/populate-entity-registry.js +3 -0
- package/.sinapse-ai/development/tasks/execute-epic-plan.md +3 -4
- package/.sinapse-ai/development/workflows/brownfield-discovery.yaml +16 -0
- package/.sinapse-ai/development/workflows/epic-orchestration.yaml +11 -1
- package/.sinapse-ai/git-hooks/lib/staged-secret-scan.js +3 -1
- package/.sinapse-ai/install-manifest.yaml +52 -80
- package/.sinapse-ai/product/templates/personalized-agent-template.md +3 -5
- package/.sinapse-ai/workflow-intelligence/index.js +3 -21
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +56 -22
- package/bin/commands/install.js +5 -4
- package/bin/lib/command-generator.js +4 -4
- package/bin/lib/fs-utils.js +18 -0
- package/bin/lib/setup-statusline.js +3 -1
- package/bin/modules/chrome-brain-installer.js +5 -3
- package/bin/modules/mcp-installer.js +3 -6
- package/bin/postinstall.js +2 -1
- package/bin/sinapse-init.js +8 -9
- package/bin/sinapse.js +3 -1
- package/bin/utils/staged-secret-scan.js +3 -1
- package/docs/framework/atlas/OPERATING-ATLAS.md +1 -1
- package/docs/framework/atlas/atlas-data.json +22 -22
- package/docs/framework/atlas/atlas.html +12 -10
- package/docs/framework/source-tree.md +1 -2
- package/package.json +3 -1
- package/packages/installer/src/config/configure-environment.js +8 -6
- package/packages/installer/src/installer/brownfield-upgrader.js +5 -3
- package/packages/installer/src/installer/file-hasher.js +29 -21
- package/packages/installer/src/installer/manifest-signature.js +61 -49
- package/packages/installer/src/installer/post-install-validator.js +24 -17
- package/packages/installer/src/merger/parsers/markdown-section-parser.js +3 -2
- package/packages/installer/src/updater/index.js +7 -1
- package/packages/installer/src/utils/atomic-write.js +35 -0
- package/packages/installer/src/wizard/index.js +5 -3
- package/packages/sinapse-install/src/capabilities/chrome-brain.js +5 -3
- package/packages/sinapse-install/src/installer.js +3 -1
- package/packages/sinapse-install/src/utils/atomic-write.js +67 -0
- package/scripts/apply-persona-disclaimer.js +2 -1
- package/scripts/eval-e2e.js +149 -0
- package/scripts/sinapse-patch.js +2 -1
- package/scripts/sync-squad-yaml-components.js +2 -1
- package/scripts/validate-agents-md.js +2 -1
- package/scripts/validate-all.js +3 -0
- package/scripts/validate-article-iv.js +289 -0
- package/scripts/validate-article-xi.js +10 -8
- package/scripts/validate-evals.js +9 -2
- package/scripts/validate-no-external-refs.js +14 -8
- package/scripts/wave-gate.js +207 -0
- package/squads/claude-code-mastery/agents/claude-mastery-chief.md +3 -8
- package/squads/claude-code-mastery/agents/config-engineer.md +5 -12
- package/squads/claude-code-mastery/agents/hooks-architect.md +5 -11
- package/squads/claude-code-mastery/agents/mcp-integrator.md +5 -12
- package/squads/claude-code-mastery/agents/project-integrator.md +5 -12
- package/squads/claude-code-mastery/agents/roadmap-sentinel.md +5 -12
- package/squads/claude-code-mastery/agents/skill-craftsman.md +5 -11
- package/squads/claude-code-mastery/agents/swarm-orqx.md +15 -28
- package/.sinapse-ai/core/execution/context-injector.js +0 -539
- package/.sinapse-ai/core/execution/parallel-executor.js +0 -301
- package/.sinapse-ai/core/execution/parallel-monitor.js +0 -432
- package/.sinapse-ai/core/execution/semantic-merge-engine.js +0 -1750
- package/.sinapse-ai/core/execution/wave-executor.js +0 -403
- package/.sinapse-ai/development/tasks/waves.md +0 -205
- package/.sinapse-ai/workflow-intelligence/engine/wave-analyzer.js +0 -692
|
@@ -752,8 +752,7 @@ outputs/ # Runtime outputs (gitignored)
|
|
|
752
752
|
│ ├── engine/ # Core WIS engines
|
|
753
753
|
│ │ ├── confidence-scorer.js # Pattern confidence scoring
|
|
754
754
|
│ │ ├── output-formatter.js # Output formatting
|
|
755
|
-
│ │
|
|
756
|
-
│ │ └── wave-analyzer.js # Wave pattern analysis
|
|
755
|
+
│ │ └── suggestion-engine.js # Intelligent suggestions
|
|
757
756
|
│ ├── learning/ # Machine learning components
|
|
758
757
|
│ │ ├── capture-hook.js # Pattern capture hooks
|
|
759
758
|
│ │ ├── index.js # Learning module entry
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sinapse-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "SINAPSE AI: Framework de orquestracao de IA — 17 squads, 172 agentes especializados",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sinapse": "bin/sinapse.js",
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
"test:coverage": "jest --coverage",
|
|
87
87
|
"test:health-check": "jest tests/health-check",
|
|
88
88
|
"eval": "node scripts/eval-runner.js",
|
|
89
|
+
"eval:e2e": "node scripts/eval-e2e.js",
|
|
89
90
|
"validate:evals": "node scripts/validate-evals.js",
|
|
90
91
|
"validate:schemas": "node scripts/validate-schemas.js",
|
|
91
92
|
"lint": "eslint . --cache --cache-location .eslintcache",
|
|
@@ -97,6 +98,7 @@
|
|
|
97
98
|
"validate:article-vii": "node scripts/validate-article-vii.js",
|
|
98
99
|
"validate:article-viii": "node scripts/validate-article-viii.js",
|
|
99
100
|
"validate:article-xi": "node scripts/validate-article-xi.js",
|
|
101
|
+
"validate:article-iv": "node scripts/validate-article-iv.js",
|
|
100
102
|
"reconcile:squads": "node scripts/reconcile-squad-manifests.js",
|
|
101
103
|
"apply:persona-disclaimer": "node scripts/apply-persona-disclaimer.js",
|
|
102
104
|
"validate:manifest": "node scripts/validate-manifest.js",
|
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
sanitizeInput,
|
|
24
24
|
} = require('./validation/config-validator');
|
|
25
25
|
const { getMergeStrategy, hasMergeStrategy } = require('../merger/index.js');
|
|
26
|
+
const { atomicWriteFileSync } = require('../utils/atomic-write');
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Configure environment files (.env and core-config.yaml)
|
|
@@ -110,7 +111,8 @@ async function configureEnvironment(options = {}) {
|
|
|
110
111
|
const merger = getMergeStrategy(envPath);
|
|
111
112
|
const mergeResult = await merger.merge(existingContent, envContent);
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
// Atomic (tmp+rename): .env carries user secrets — never leave it torn
|
|
115
|
+
atomicWriteFileSync(envPath, mergeResult.content, 'utf8');
|
|
114
116
|
results.envCreated = true;
|
|
115
117
|
|
|
116
118
|
console.log('✅ Merged .env file');
|
|
@@ -119,8 +121,8 @@ async function configureEnvironment(options = {}) {
|
|
|
119
121
|
console.log(` ⚠️ Suggestions: ${mergeResult.stats.conflicts} (see comments in file)`);
|
|
120
122
|
}
|
|
121
123
|
} else {
|
|
122
|
-
// Create new or overwrite
|
|
123
|
-
|
|
124
|
+
// Create new or overwrite — atomic (tmp+rename)
|
|
125
|
+
atomicWriteFileSync(envPath, envContent, 'utf8');
|
|
124
126
|
results.envCreated = true;
|
|
125
127
|
console.log('✅ Created .env file');
|
|
126
128
|
}
|
|
@@ -133,7 +135,7 @@ async function configureEnvironment(options = {}) {
|
|
|
133
135
|
// Step 4: Generate and write .env.example
|
|
134
136
|
const envExamplePath = path.join(targetDir, '.env.example');
|
|
135
137
|
const envExampleContent = generateEnvExample();
|
|
136
|
-
|
|
138
|
+
atomicWriteFileSync(envExamplePath, envExampleContent, 'utf8');
|
|
137
139
|
results.envExampleCreated = true;
|
|
138
140
|
console.log('✅ Created .env.example file');
|
|
139
141
|
|
|
@@ -187,7 +189,7 @@ async function configureEnvironment(options = {}) {
|
|
|
187
189
|
throw new Error('Generated core-config.yaml has invalid structure');
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
|
|
192
|
+
atomicWriteFileSync(coreConfigPath, coreConfigContent, 'utf8');
|
|
191
193
|
results.coreConfigCreated = true;
|
|
192
194
|
console.log('✅ Created .sinapse-ai/core-config.yaml');
|
|
193
195
|
|
|
@@ -359,7 +361,7 @@ async function updateGitignore(targetDir) {
|
|
|
359
361
|
|
|
360
362
|
newContent += '\n';
|
|
361
363
|
|
|
362
|
-
|
|
364
|
+
atomicWriteFileSync(gitignorePath, newContent, 'utf8');
|
|
363
365
|
}
|
|
364
366
|
}
|
|
365
367
|
|
|
@@ -12,6 +12,7 @@ const yaml = require('js-yaml');
|
|
|
12
12
|
const semver = require('semver');
|
|
13
13
|
const { hashFile, hashesMatch } = require('./file-hasher');
|
|
14
14
|
const { YamlMerger } = require('../merger/strategies/yaml-merger.js');
|
|
15
|
+
const { atomicWriteFileSync } = require('../utils/atomic-write');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Upgrade report structure
|
|
@@ -278,8 +279,8 @@ async function applyUpgrade(report, sourceDir, targetDir, options = {}) {
|
|
|
278
279
|
const merger = new YamlMerger();
|
|
279
280
|
const mergeResult = await merger.merge(sourceContent, targetContent);
|
|
280
281
|
|
|
281
|
-
// Write merged content
|
|
282
|
-
|
|
282
|
+
// Write merged content — atomic (tmp+rename); backup taken above
|
|
283
|
+
atomicWriteFileSync(targetPath, mergeResult.content, 'utf8');
|
|
283
284
|
|
|
284
285
|
// Log conflict warnings
|
|
285
286
|
const conflicts = mergeResult.changes.filter(c => c.type === 'conflict');
|
|
@@ -379,7 +380,8 @@ function updateInstalledManifest(targetDir, sourceManifest, sourcePackage) {
|
|
|
379
380
|
`;
|
|
380
381
|
|
|
381
382
|
fs.ensureDirSync(path.dirname(installedManifestPath));
|
|
382
|
-
|
|
383
|
+
// Atomic (tmp+rename): the installed manifest drives future upgrades
|
|
384
|
+
atomicWriteFileSync(installedManifestPath, header + yamlContent, 'utf8');
|
|
383
385
|
|
|
384
386
|
return installedManifestPath;
|
|
385
387
|
}
|
|
@@ -62,24 +62,29 @@ function removeBOM(content) {
|
|
|
62
62
|
* @throws {Error} - If file cannot be read
|
|
63
63
|
*/
|
|
64
64
|
function hashFile(filePath) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
// SECURITY [TOCTOU]: single read — the hashed bytes are exactly what was
|
|
66
|
+
// read, with no exists/stat/read window for a concurrent writer to exploit.
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = fs.readFileSync(filePath);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error.code === 'ENOENT') {
|
|
72
|
+
throw new Error(`File not found: ${filePath}`);
|
|
73
|
+
}
|
|
74
|
+
if (error.code === 'EISDIR') {
|
|
75
|
+
throw new Error(`Cannot hash directory: ${filePath}`);
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
let content;
|
|
75
81
|
|
|
76
82
|
if (isBinaryFile(filePath)) {
|
|
77
83
|
// Binary files: hash raw bytes
|
|
78
|
-
content =
|
|
84
|
+
content = raw;
|
|
79
85
|
} else {
|
|
80
86
|
// Text files: normalize line endings and remove BOM
|
|
81
|
-
const
|
|
82
|
-
const withoutBOM = removeBOM(rawContent);
|
|
87
|
+
const withoutBOM = removeBOM(raw.toString('utf8'));
|
|
83
88
|
const normalized = normalizeLineEndings(withoutBOM);
|
|
84
89
|
content = Buffer.from(normalized, 'utf8');
|
|
85
90
|
}
|
|
@@ -96,25 +101,28 @@ function hashFile(filePath) {
|
|
|
96
101
|
* @throws {Error} - If file cannot be read
|
|
97
102
|
*/
|
|
98
103
|
async function hashFileAsync(filePath) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// SECURITY [TOCTOU]: single read (see hashFile) — no exists/stat/read window.
|
|
105
|
+
let raw;
|
|
106
|
+
try {
|
|
107
|
+
raw = await fs.readFile(filePath);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error.code === 'ENOENT') {
|
|
110
|
+
throw new Error(`File not found: ${filePath}`);
|
|
111
|
+
}
|
|
112
|
+
if (error.code === 'EISDIR') {
|
|
113
|
+
throw new Error(`Cannot hash directory: ${filePath}`);
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
let content;
|
|
110
119
|
|
|
111
120
|
if (isBinaryFile(filePath)) {
|
|
112
121
|
// Binary files: hash raw bytes
|
|
113
|
-
content =
|
|
122
|
+
content = raw;
|
|
114
123
|
} else {
|
|
115
124
|
// Text files: normalize line endings and remove BOM
|
|
116
|
-
const
|
|
117
|
-
const withoutBOM = removeBOM(rawContent);
|
|
125
|
+
const withoutBOM = removeBOM(raw.toString('utf8'));
|
|
118
126
|
const normalized = normalizeLineEndings(withoutBOM);
|
|
119
127
|
content = Buffer.from(normalized, 'utf8');
|
|
120
128
|
}
|
|
@@ -285,82 +285,94 @@ function signatureExists(manifestPath) {
|
|
|
285
285
|
* @param {Object} [options.publicKey] - Override public key (testing only)
|
|
286
286
|
* @returns {Object} { content: Buffer, verified: boolean, error: string|null }
|
|
287
287
|
*/
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Open + fstat + read on a SINGLE file descriptor.
|
|
290
|
+
*
|
|
291
|
+
* SECURITY [TOCTOU]: size check (DoS guard) and read happen on the same open
|
|
292
|
+
* fd, so a concurrent writer cannot swap the file between the check and the
|
|
293
|
+
* use — the buffer verified downstream is exactly the bytes that were sized.
|
|
294
|
+
*
|
|
295
|
+
* @param {string} filePath - File to read
|
|
296
|
+
* @param {number} maxSize - Maximum allowed size in bytes
|
|
297
|
+
* @param {string} label - Human label used in error messages (e.g. "Manifest file")
|
|
298
|
+
* @returns {{content: Buffer|null, error: string|null, missing: boolean}}
|
|
299
|
+
*/
|
|
300
|
+
function readFileCapped(filePath, maxSize, label) {
|
|
301
|
+
let fd;
|
|
302
|
+
try {
|
|
303
|
+
fd = fs.openSync(filePath, 'r');
|
|
304
|
+
} catch (error) {
|
|
294
305
|
return {
|
|
295
306
|
content: null,
|
|
296
|
-
|
|
297
|
-
|
|
307
|
+
error: `Cannot read ${label.toLowerCase()}: ${error.message}`,
|
|
308
|
+
missing: error.code === 'ENOENT',
|
|
298
309
|
};
|
|
299
310
|
}
|
|
300
|
-
|
|
301
|
-
// SECURITY [DOS-1]: Check manifest file size BEFORE reading into memory
|
|
302
|
-
// This prevents DoS attacks via oversized manifest files
|
|
303
|
-
let manifestStat;
|
|
304
311
|
try {
|
|
305
|
-
|
|
312
|
+
const stat = fs.fstatSync(fd);
|
|
313
|
+
if (stat.size > maxSize) {
|
|
314
|
+
return {
|
|
315
|
+
content: null,
|
|
316
|
+
error: `${label} exceeds maximum size (${maxSize} bytes)`,
|
|
317
|
+
missing: false,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const content = Buffer.alloc(stat.size);
|
|
321
|
+
fs.readSync(fd, content, 0, stat.size, 0);
|
|
322
|
+
return { content, error: null, missing: false };
|
|
306
323
|
} catch (error) {
|
|
307
324
|
return {
|
|
308
325
|
content: null,
|
|
309
|
-
|
|
310
|
-
|
|
326
|
+
error: `Cannot read ${label.toLowerCase()}: ${error.message}`,
|
|
327
|
+
missing: false,
|
|
311
328
|
};
|
|
329
|
+
} finally {
|
|
330
|
+
fs.closeSync(fd);
|
|
312
331
|
}
|
|
332
|
+
}
|
|
313
333
|
|
|
314
|
-
|
|
334
|
+
function loadAndVerifyManifest(manifestPath, options = {}) {
|
|
335
|
+
const requireSignature = options.requireSignature !== false;
|
|
336
|
+
const signaturePath = manifestPath + '.minisig';
|
|
337
|
+
|
|
338
|
+
// SECURITY [DOS-1] + [TOCTOU]: size is validated on the same fd that is
|
|
339
|
+
// read, so the verified buffer cannot be swapped after the check.
|
|
340
|
+
const manifest = readFileCapped(manifestPath, SignatureLimits.MAX_MANIFEST_SIZE, 'Manifest file');
|
|
341
|
+
if (manifest.content === null) {
|
|
315
342
|
return {
|
|
316
343
|
content: null,
|
|
317
344
|
verified: false,
|
|
318
|
-
error:
|
|
345
|
+
error: manifest.missing ? 'Manifest file not found' : manifest.error,
|
|
319
346
|
};
|
|
320
347
|
}
|
|
321
348
|
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
349
|
+
// SECURITY [DOS-2] + [TOCTOU]: same single-fd pattern for the signature
|
|
350
|
+
const signature = readFileCapped(signaturePath, SignatureLimits.MAX_SIGNATURE_SIZE, 'Signature file');
|
|
351
|
+
if (signature.content === null) {
|
|
352
|
+
if (signature.missing) {
|
|
353
|
+
if (requireSignature) {
|
|
354
|
+
return {
|
|
355
|
+
content: null,
|
|
356
|
+
verified: false,
|
|
357
|
+
error: 'Manifest signature file not found (.minisig)',
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
// Allow unsigned in dev mode (requireSignature=false)
|
|
325
361
|
return {
|
|
326
|
-
content:
|
|
362
|
+
content: manifest.content,
|
|
327
363
|
verified: false,
|
|
328
|
-
error:
|
|
364
|
+
error: null,
|
|
329
365
|
};
|
|
330
366
|
}
|
|
331
|
-
// Allow unsigned in dev mode (requireSignature=false)
|
|
332
|
-
// Size already validated above
|
|
333
|
-
return {
|
|
334
|
-
content: fs.readFileSync(manifestPath),
|
|
335
|
-
verified: false,
|
|
336
|
-
error: null,
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// SECURITY [DOS-2]: Check signature file size BEFORE reading
|
|
341
|
-
// Signature files should be small (~200 bytes typical)
|
|
342
|
-
let signatureStat;
|
|
343
|
-
try {
|
|
344
|
-
signatureStat = fs.statSync(signaturePath);
|
|
345
|
-
} catch (error) {
|
|
346
|
-
return {
|
|
347
|
-
content: null,
|
|
348
|
-
verified: false,
|
|
349
|
-
error: `Cannot stat signature file: ${error.message}`,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (signatureStat.size > SignatureLimits.MAX_SIGNATURE_SIZE) {
|
|
354
367
|
return {
|
|
355
368
|
content: null,
|
|
356
369
|
verified: false,
|
|
357
|
-
error:
|
|
370
|
+
error: signature.error,
|
|
358
371
|
};
|
|
359
372
|
}
|
|
360
373
|
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
const signatureContent = fs.readFileSync(signaturePath, 'utf8');
|
|
374
|
+
const manifestContent = manifest.content;
|
|
375
|
+
const signatureContent = signature.content.toString('utf8');
|
|
364
376
|
|
|
365
377
|
// Verify signature BEFORE any parsing
|
|
366
378
|
const verifyResult = verifyManifestSignature(manifestContent, signatureContent, options);
|
|
@@ -456,21 +456,30 @@ class PostInstallValidator {
|
|
|
456
456
|
// Development mode: signature not required (NOT for production)
|
|
457
457
|
this.log('WARNING: Signature verification disabled - development mode only');
|
|
458
458
|
try {
|
|
459
|
-
// SECURITY [DOS-3]:
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
459
|
+
// SECURITY [DOS-3] + [TOCTOU]: fstat + read on the same open fd, so the
|
|
460
|
+
// size check and the parsed bytes refer to the same file version.
|
|
461
|
+
const fd = fs.openSync(manifestPath, 'r');
|
|
462
|
+
let content;
|
|
463
|
+
try {
|
|
464
|
+
const manifestStat = fs.fstatSync(fd);
|
|
465
|
+
if (manifestStat.size > SecurityLimits.MAX_MANIFEST_SIZE) {
|
|
466
|
+
this.issues.push({
|
|
467
|
+
type: IssueType.INVALID_MANIFEST,
|
|
468
|
+
severity: Severity.CRITICAL,
|
|
469
|
+
message: 'Manifest file exceeds maximum size',
|
|
470
|
+
details: `Size: ${manifestStat.size} bytes, Max: ${SecurityLimits.MAX_MANIFEST_SIZE} bytes`,
|
|
471
|
+
remediation: 'Use a valid manifest file from the official source',
|
|
472
|
+
relativePath: null,
|
|
473
|
+
});
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
472
476
|
|
|
473
|
-
|
|
477
|
+
const buf = Buffer.alloc(manifestStat.size);
|
|
478
|
+
fs.readSync(fd, buf, 0, manifestStat.size, 0);
|
|
479
|
+
content = buf.toString('utf8');
|
|
480
|
+
} finally {
|
|
481
|
+
fs.closeSync(fd);
|
|
482
|
+
}
|
|
474
483
|
return this.parseManifestContent(content);
|
|
475
484
|
} catch (error) {
|
|
476
485
|
this.issues.push({
|
|
@@ -1394,9 +1403,7 @@ class PostInstallValidator {
|
|
|
1394
1403
|
*/
|
|
1395
1404
|
function formatReport(report, options = {}) {
|
|
1396
1405
|
const useColors = options.colors !== false;
|
|
1397
|
-
// Note: detailed
|
|
1398
|
-
// eslint-disable-next-line no-unused-vars
|
|
1399
|
-
const detailed = options.detailed === true;
|
|
1406
|
+
// Note: options.detailed is parsed from CLI but the detailed view is handled by the caller
|
|
1400
1407
|
|
|
1401
1408
|
const c = {
|
|
1402
1409
|
reset: useColors ? '\x1b[0m' : '',
|
|
@@ -140,8 +140,9 @@ function parseMarkdownSections(content) {
|
|
|
140
140
|
result.preamble.push(line);
|
|
141
141
|
} else if (currentSection) {
|
|
142
142
|
currentSection.lines.push(line);
|
|
143
|
-
} else
|
|
144
|
-
// Content after an SINAPSE section but before next section
|
|
143
|
+
} else {
|
|
144
|
+
// Content after an SINAPSE section but before next section (sinapseSection
|
|
145
|
+
// is always null here — the in-section branch above `continue`s).
|
|
145
146
|
// This shouldn't happen in well-formed files, but handle it
|
|
146
147
|
result.preamble.push(line);
|
|
147
148
|
}
|
|
@@ -780,11 +780,17 @@ class SINAPSEUpdater {
|
|
|
780
780
|
/**
|
|
781
781
|
* Log if verbose
|
|
782
782
|
*
|
|
783
|
+
* Messages may interpolate error text or npm registry responses; control
|
|
784
|
+
* characters (ANSI escapes, CR) are stripped so external data cannot forge
|
|
785
|
+
* log lines in the user's terminal (CWE-117).
|
|
786
|
+
*
|
|
783
787
|
* @param {string} message - Message
|
|
784
788
|
*/
|
|
785
789
|
log(message) {
|
|
786
790
|
if (this.options.verbose) {
|
|
787
|
-
|
|
791
|
+
// eslint-disable-next-line no-control-regex
|
|
792
|
+
const sanitized = String(message).replace(/[\x00-\x08\x0B-\x1F\x7F]/g, '');
|
|
793
|
+
console.log(`[SINAPSEUpdater] ${sanitized}`);
|
|
788
794
|
}
|
|
789
795
|
}
|
|
790
796
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic write bridge — Onda B2 (AF-20260703)
|
|
3
|
+
*
|
|
4
|
+
* Thin pointer to the canonical tmp+rename util in
|
|
5
|
+
* `.sinapse-ai/core/synapse/utils/atomic-write.js` (ships in the same npm
|
|
6
|
+
* package as this workspace). Fail-open: if the canonical util can't be
|
|
7
|
+
* resolved, fall back to a plain write (the pre-hardening behavior) rather
|
|
8
|
+
* than breaking the installer.
|
|
9
|
+
*
|
|
10
|
+
* @module src/utils/atomic-write
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
let _atomicWriteSync = null;
|
|
19
|
+
try {
|
|
20
|
+
// packages/installer/src/utils → repo/package root is 4 levels up
|
|
21
|
+
_atomicWriteSync = require(path.join(
|
|
22
|
+
__dirname, '..', '..', '..', '..',
|
|
23
|
+
'.sinapse-ai', 'core', 'synapse', 'utils', 'atomic-write',
|
|
24
|
+
)).atomicWriteSync;
|
|
25
|
+
} catch { /* fallback below */ }
|
|
26
|
+
|
|
27
|
+
function atomicWriteFileSync(filePath, data, encoding = 'utf8') {
|
|
28
|
+
if (_atomicWriteSync) {
|
|
29
|
+
_atomicWriteSync(filePath, data, encoding);
|
|
30
|
+
} else {
|
|
31
|
+
fs.writeFileSync(filePath, data, encoding);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { atomicWriteFileSync };
|
|
@@ -13,6 +13,7 @@ const path = require('path');
|
|
|
13
13
|
const fse = require('fs-extra');
|
|
14
14
|
const { execSync } = require('child_process');
|
|
15
15
|
const { colors } = require('../utils/sinapse-colors');
|
|
16
|
+
const { atomicWriteFileSync } = require('../utils/atomic-write');
|
|
16
17
|
const {
|
|
17
18
|
getLanguageQuestion,
|
|
18
19
|
getLLMQuestion,
|
|
@@ -78,7 +79,8 @@ async function writeClaudeSettings(language, projectDir = process.cwd()) {
|
|
|
78
79
|
const claudeLanguage = LANGUAGE_MAP[language] || language;
|
|
79
80
|
settings.language = claudeLanguage;
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
// Atomic (tmp+rename): user settings.json must never be torn
|
|
83
|
+
atomicWriteFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
82
84
|
return true;
|
|
83
85
|
} catch {
|
|
84
86
|
// Non-blocking: language is a preference, not critical
|
|
@@ -366,7 +368,7 @@ async function installGlobalAgents() {
|
|
|
366
368
|
try {
|
|
367
369
|
const content = buildAgentTemplate(agent.id, agent.squad, homedir);
|
|
368
370
|
const filePath = path.join(agentsDir, `${agent.id}.md`);
|
|
369
|
-
|
|
371
|
+
atomicWriteFileSync(filePath, content, 'utf8');
|
|
370
372
|
count++;
|
|
371
373
|
} catch (err) {
|
|
372
374
|
errors.push(`${agent.id}: ${err.message}`);
|
|
@@ -650,7 +652,7 @@ async function runWizard(options = {}) {
|
|
|
650
652
|
'# User-Defined Preferred Patterns and Preferences',
|
|
651
653
|
'# User-Defined Preferred Patterns and Preferences' + activePresetSection,
|
|
652
654
|
);
|
|
653
|
-
|
|
655
|
+
atomicWriteFileSync(techPrefsFile, techPrefsContent, 'utf8');
|
|
654
656
|
}
|
|
655
657
|
}
|
|
656
658
|
}
|
|
@@ -19,6 +19,7 @@ const path = require('path');
|
|
|
19
19
|
const os = require('os');
|
|
20
20
|
const { execSync } = require('child_process');
|
|
21
21
|
const { detectOS, OS_TYPE } = require('../os-detector');
|
|
22
|
+
const { atomicWriteFileSync } = require('../utils/atomic-write');
|
|
22
23
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Constants
|
|
@@ -339,7 +340,8 @@ function readJsonSafe(filePath) {
|
|
|
339
340
|
function writeJson(filePath, data) {
|
|
340
341
|
const dir = path.dirname(filePath);
|
|
341
342
|
fs.mkdirSync(dir, { recursive: true });
|
|
342
|
-
|
|
343
|
+
// Atomic (tmp+rename): these are user config files (settings.json, .mcp.json)
|
|
344
|
+
atomicWriteFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
343
345
|
}
|
|
344
346
|
|
|
345
347
|
/**
|
|
@@ -755,7 +757,7 @@ function installChromeBrain(options = {}) {
|
|
|
755
757
|
if (dryRun) {
|
|
756
758
|
LOG.info(`[DRY-RUN] Would create ${filePath}`);
|
|
757
759
|
} else {
|
|
758
|
-
|
|
760
|
+
atomicWriteFileSync(filePath, content, 'utf8');
|
|
759
761
|
// Make executable on Unix
|
|
760
762
|
if (osInfo.type !== OS_TYPE.WINDOWS) {
|
|
761
763
|
fs.chmodSync(filePath, 0o755);
|
|
@@ -848,7 +850,7 @@ function installChromeBrain(options = {}) {
|
|
|
848
850
|
LOG.info(`[DRY-RUN] Would create ${file.label}`);
|
|
849
851
|
} else {
|
|
850
852
|
fs.mkdirSync(path.dirname(file.path), { recursive: true });
|
|
851
|
-
|
|
853
|
+
atomicWriteFileSync(file.path, file.content, 'utf8');
|
|
852
854
|
LOG.ok(file.label);
|
|
853
855
|
}
|
|
854
856
|
} catch (err) {
|
|
@@ -23,6 +23,7 @@ const { intro, outro, select, confirm, note, isCancel, cancel } = require('@clac
|
|
|
23
23
|
const { execa } = require('execa');
|
|
24
24
|
|
|
25
25
|
const { detectOS, getOSDisplayName } = require('./os-detector');
|
|
26
|
+
const { atomicWriteFileSync } = require('./utils/atomic-write');
|
|
26
27
|
const { checkAllDependencies, displayResults } = require('./dep-checker');
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -148,7 +149,8 @@ async function createUserConfigDirect(profile, logger, dryRun) {
|
|
|
148
149
|
|
|
149
150
|
const yaml = require('js-yaml');
|
|
150
151
|
const yamlContent = yaml.dump(config, { lineWidth: -1 });
|
|
151
|
-
|
|
152
|
+
// Atomic (tmp+rename): user config must never be torn
|
|
153
|
+
atomicWriteFileSync(userConfigPath, yamlContent, 'utf8');
|
|
152
154
|
|
|
153
155
|
logger.success(`User config created at ${userConfigPath}`);
|
|
154
156
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic write (tmp + rename) — self-contained copy for @sinapse/sinapse-install.
|
|
3
|
+
*
|
|
4
|
+
* This package publishes on its own, so it cannot require the canonical util
|
|
5
|
+
* in `.sinapse-ai/core/synapse/utils/atomic-write.js` (main sinapse-ai
|
|
6
|
+
* package). Keep the two in sync if the pattern evolves.
|
|
7
|
+
*
|
|
8
|
+
* Onda B2 (AF-20260703): a crash or concurrent reader must never see a torn
|
|
9
|
+
* config file.
|
|
10
|
+
*
|
|
11
|
+
* @module src/utils/atomic-write
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
20
|
+
const TRANSIENT_LOCK_CODES = new Set(['EPERM', 'EBUSY', 'EACCES', 'ENOTEMPTY']);
|
|
21
|
+
|
|
22
|
+
function sleepSync(ms) {
|
|
23
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function retrySync(fn, retries = 3, delayMs = 120) {
|
|
27
|
+
for (let attempt = 0; ; attempt++) {
|
|
28
|
+
try {
|
|
29
|
+
return fn();
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (!TRANSIENT_LOCK_CODES.has(err.code) || attempt >= retries) {
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
sleepSync(delayMs * (attempt + 1));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function atomicWriteFileSync(filePath, data, encoding = 'utf8') {
|
|
40
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
41
|
+
try {
|
|
42
|
+
const dir = path.dirname(filePath);
|
|
43
|
+
if (!fs.existsSync(dir)) {
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
fs.writeFileSync(tmpPath, data, encoding);
|
|
47
|
+
if (IS_WINDOWS) {
|
|
48
|
+
retrySync(() => {
|
|
49
|
+
try {
|
|
50
|
+
fs.unlinkSync(filePath);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err.code !== 'ENOENT') {
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
retrySync(() => fs.renameSync(tmpPath, filePath));
|
|
59
|
+
} catch (error) {
|
|
60
|
+
try {
|
|
61
|
+
fs.unlinkSync(tmpPath);
|
|
62
|
+
} catch { /* ignore cleanup errors */ }
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { atomicWriteFileSync };
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
|
+
const { atomicWriteSync } = require('../.sinapse-ai/core/synapse/utils/atomic-write');
|
|
19
20
|
|
|
20
21
|
const ROOT = path.resolve(__dirname, '..');
|
|
21
22
|
|
|
@@ -85,7 +86,7 @@ function ensureNotice(filePath, realName) {
|
|
|
85
86
|
next = notice + '\n\n' + content;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
atomicWriteSync(filePath, next);
|
|
89
90
|
return { status: 'inserted', filePath };
|
|
90
91
|
}
|
|
91
92
|
|