Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .aiox-core/install-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
# - SHA256 hashes for change detection
# - File types for categorization
#
version: 5.1.3
generated_at: "2026-05-07T10:57:49.732Z"
version: 5.1.4
generated_at: "2026-05-07T11:20:24.845Z"
generator: scripts/generate-install-manifest.js
file_count: 1103
files:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Story 123.6: Validate tolera artefatos mutáveis do install

## Status

- [x] Rascunho
- [x] Em implementação
- [x] Concluída

## Contexto

O smoke test do pacote publicado `@aiox-squads/core@5.1.3` confirmou que um install limpo sobe o CLI corretamente, mas `aiox validate --json` reporta `corruptedFiles: 2` logo após o próprio `aiox install --force --quiet`.

Issue GitHub: SynkraAI/aiox-core#663.

## Objetivo

Fazer o validador distinguir arquivos de framework imutáveis de artefatos mutáveis que o instalador gera ou mescla durante a instalação, sem reduzir as validações de path, existência e segurança.

## Acceptance Criteria

- [x] AC1. `aiox validate` não marca `.aiox-core/core-config.yaml` como corrompido quando o próprio instalador gera ou mescla o arquivo.
- [x] AC2. `aiox validate` não marca `.aiox-core/data/entity-registry.yaml` como corrompido quando o bootstrap da registry popula o arquivo.
- [x] AC3. Arquivos mutáveis continuam validados quanto a existência, contenção de path, symlink e tipo regular.
- [x] AC4. Hash/tamanho continuam rígidos para arquivos não mutáveis do framework.
- [x] AC5. Smoke test publicado ou local equivalente termina com `corruptedFiles: 0`.

## Tasks

- [x] Adicionar classificação explícita de artefatos mutáveis no `PostInstallValidator`.
- [x] Ignorar comparação rígida de hash/tamanho apenas para os paths mutáveis conhecidos.
- [x] Cobrir regressão com teste unitário de install manifest pós-mutação.
- [x] Rodar gates locais focados e completos antes de abrir PR.

## Execution

- `npm test -- tests/installer/post-install-validator.test.js --runInBand` ✓
- `node -c packages/installer/src/installer/post-install-validator.js` ✓
- `npm run generate:manifest && npm run validate:manifest` ✓
- Smoke de pacote local `@aiox-squads/core@5.1.4`:
- `npx --yes aiox --version` → `5.1.4`
- `npx --yes aiox validate --json` → `status: success`, `validFiles: 1103`, `corruptedFiles: 0`, `issueCount: 0`

## File List

- [docs/stories/epic-123/STORY-123.6-validate-mutable-install-artifacts.md](./STORY-123.6-validate-mutable-install-artifacts.md)
- [.aiox-core/install-manifest.yaml](../../../.aiox-core/install-manifest.yaml)
- [package.json](../../../package.json)
- [package-lock.json](../../../package-lock.json)
- [packages/installer/src/installer/post-install-validator.js](../../../packages/installer/src/installer/post-install-validator.js)
- [tests/installer/post-install-validator.test.js](../../../tests/installer/post-install-validator.test.js)
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aiox-squads/core",
"version": "5.1.3",
"version": "5.1.4",
"description": "Synkra AIOX: AI-Orchestrated System for Full Stack Development - Core Framework",
"bin": {
"aiox": "bin/aiox.js",
Expand Down
27 changes: 27 additions & 0 deletions packages/installer/src/installer/post-install-validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ const ALLOWED_TYPE_VALUES = [
'manifest',
];

const PROJECT_MUTABLE_MANIFEST_PATHS = new Set([
'core-config.yaml',
'data/entity-registry.yaml',
]);

/**
* Categorize a file path into its functional category
* @param {string} filePath - Relative file path
Expand Down Expand Up @@ -169,6 +174,19 @@ function getSeverityForCategory(category) {
return severityMap[category] || Severity.LOW;
}

/**
* Determine whether a manifest path is expected to be mutated by install/bootstrap.
* These files still go through path/existence/symlink validation, but their content
* is project-local after install and must not be hash-compared to the package copy.
*
* @param {string} filePath - Relative file path from .aiox-core
* @returns {boolean} True when the file is project mutable
*/
function isProjectMutableManifestPath(filePath) {
const normalized = String(filePath || '').replace(/\\/g, '/');
return PROJECT_MUTABLE_MANIFEST_PATHS.has(normalized);
}

/**
* Validate that a resolved path is contained within the root directory
* Prevents path traversal attacks via malicious manifest entries
Expand Down Expand Up @@ -787,6 +805,12 @@ class PostInstallValidator {

result.exists = true;

if (isProjectMutableManifestPath(relativePath)) {
result.projectMutable = true;
this.stats.validFiles++;
return result;
}

// SECURITY [H2]: In quick mode (no hash), size MUST be present
if (!this.options.verifyHashes) {
if (entry.size === null || entry.size === undefined) {
Expand Down Expand Up @@ -1077,6 +1101,8 @@ class PostInstallValidator {
if (issuesBySeverity[Severity.CRITICAL].length > 0) {
status = 'failed';
} else if (
this.stats.missingFiles > 0 ||
this.stats.corruptedFiles > 0 ||
issuesBySeverity[Severity.HIGH].length > 0 ||
issuesBySeverity[Severity.MEDIUM].length > 0
) {
Expand Down Expand Up @@ -1521,5 +1547,6 @@ module.exports = {
FileCategory,
SecurityLimits,
isPathContained,
isProjectMutableManifestPath,
validateManifestEntry,
};
67 changes: 67 additions & 0 deletions tests/installer/post-install-validator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const {
IssueType,
Severity,
SecurityLimits,
isProjectMutableManifestPath,
} = require('../../packages/installer/src/installer/post-install-validator');

describe('PostInstallValidator Security Tests', () => {
Expand Down Expand Up @@ -360,6 +361,72 @@ describe('PostInstallValidator Security Tests', () => {
});
});

describe('Project Mutable Install Artifacts', () => {
test('should classify only install-mutated manifest paths as project mutable', () => {
expect(isProjectMutableManifestPath('core-config.yaml')).toBe(true);
expect(isProjectMutableManifestPath('data/entity-registry.yaml')).toBe(true);
expect(isProjectMutableManifestPath('data/capability-detection.js')).toBe(false);
expect(isProjectMutableManifestPath('core/config.js')).toBe(false);
});

test('should not mark installer-mutated files as corrupted after content changes', async () => {
const manifest = `version: "1.0.0"
files:
- path: core-config.yaml
hash: "sha256:${'a'.repeat(64)}"
size: 10
- path: data/entity-registry.yaml
hash: "sha256:${'b'.repeat(64)}"
size: 10`;

await fs.ensureDir(path.join(targetDir, '.aiox-core', 'data'));
await fs.writeFile(path.join(targetDir, '.aiox-core', 'install-manifest.yaml'), manifest);
await fs.writeFile(path.join(targetDir, '.aiox-core', 'core-config.yaml'), 'project:\n name: generated\n');
await fs.writeFile(
path.join(targetDir, '.aiox-core', 'data', 'entity-registry.yaml'),
'metadata:\n entityCount: 757\n updatedAt: now\n',
);

const validator = new PostInstallValidator(targetDir, null, {
requireSignature: false,
verifyHashes: true,
});

const report = await validator.validate();

expect(report.status).toBe('success');
expect(report.stats.validFiles).toBe(2);
expect(report.stats.corruptedFiles).toBe(0);
expect(report.issues).toHaveLength(0);
});

test('should still report corruption for non-mutable manifest paths', async () => {
const manifest = `version: "1.0.0"
files:
- path: data/capability-detection.js
hash: "sha256:${'c'.repeat(64)}"
size: 10`;

await fs.ensureDir(path.join(targetDir, '.aiox-core', 'data'));
await fs.writeFile(path.join(targetDir, '.aiox-core', 'install-manifest.yaml'), manifest);
await fs.writeFile(
path.join(targetDir, '.aiox-core', 'data', 'capability-detection.js'),
'module.exports = {};\n',
);

const validator = new PostInstallValidator(targetDir, null, {
requireSignature: false,
verifyHashes: true,
});

const report = await validator.validate();

expect(report.status).toBe('warning');
expect(report.stats.corruptedFiles).toBe(1);
expect(report.issues[0].type).toBe(IssueType.CORRUPTED_FILE);
});
});

describe('Secure Repair (C4)', () => {
test('should refuse repair without hash verification', async () => {
const validator = new PostInstallValidator(targetDir, sourceDir, {
Expand Down
Loading