aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
301 lines (243 loc) • 8.29 kB
JavaScript
#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const CORE_CONFIG_FILE = '.aios-core/core-config.yaml';
const SETTINGS_FILE = '.claude/settings.json';
const TOOLS = ['Edit', 'Write'];
/**
* Validate that a boundary path does not escape the project root via traversal.
* Rejects paths containing '..' segments or absolute paths.
*/
function validateBoundaryPath(p) {
const segments = p.replace(/\\/g, '/').split('/');
if (segments.some(function(s) { return s === '..'; })) {
throw new Error('Path traversal detected in boundary config: ' + p);
}
if (path.isAbsolute(p)) {
throw new Error('Absolute path not allowed in boundary config: ' + p);
}
}
/**
* Read boundary configuration from core-config.yaml.
*/
function readBoundaryConfig(projectRoot) {
const configPath = path.join(projectRoot, CORE_CONFIG_FILE);
if (!fs.existsSync(configPath)) {
throw new Error(`core-config.yaml not found at ${configPath}`);
}
const content = fs.readFileSync(configPath, 'utf8');
const config = yaml.load(content);
if (!config || !config.boundary) {
throw new Error('core-config.yaml missing "boundary" section');
}
const { boundary } = config;
const result = {
frameworkProtection: boundary.frameworkProtection !== false,
protected: Array.isArray(boundary.protected) ? boundary.protected : [],
exceptions: Array.isArray(boundary.exceptions) ? boundary.exceptions : [],
};
for (const p of result.protected) { validateBoundaryPath(p); }
for (const p of result.exceptions) { validateBoundaryPath(p); }
return result;
}
/**
* Check if an exception path falls within a given directory.
*/
function isChildOf(exceptionPath, parentDir) {
const clean = exceptionPath.replace(/\/\*\*$/, '');
const parts = clean.split('/');
const parentParts = parentDir.split('/');
if (parts.length <= parentParts.length) {
return false;
}
for (let i = 0; i < parentParts.length; i++) {
if (parts[i] !== parentParts[i]) {
return false;
}
}
return true;
}
/**
* Expand a directory one level deep into subdirectory/** and file entries.
* Returns { dirs: [...], files: [...] } sorted separately.
*/
function expandOneLevel(dirRelative, projectRoot) {
const dirAbsolute = path.join(projectRoot, dirRelative);
if (!fs.existsSync(dirAbsolute)) {
return { dirs: [dirRelative + '/**'], files: [] };
}
const entries = fs.readdirSync(dirAbsolute, { withFileTypes: true });
const dirs = [];
const files = [];
for (const entry of entries) {
const entryRelative = dirRelative + '/' + entry.name;
if (entry.isDirectory()) {
dirs.push(entryRelative);
} else {
files.push(entryRelative);
}
}
dirs.sort();
files.sort();
return { dirs, files };
}
/**
* For a subdirectory that has exceptions inside it, expand to individual
* file-level deny rules excluding files/dirs covered by exceptions.
*/
function expandSubdirWithExceptions(subdirPath, exceptions, projectRoot) {
const dirAbsolute = path.join(projectRoot, subdirPath);
if (!fs.existsSync(dirAbsolute)) {
return [subdirPath + '/**'];
}
const entries = fs.readdirSync(dirAbsolute, { withFileTypes: true });
const result = [];
for (const entry of entries) {
const entryRelative = subdirPath + '/' + entry.name;
const entryGlob = entry.isDirectory() ? entryRelative + '/**' : entryRelative;
const isCoveredByException = exceptions.some(function(exc) {
return exc === entryGlob || exc === entryRelative;
});
if (!isCoveredByException) {
result.push(entryGlob);
}
}
result.sort();
return result;
}
/**
* Expand protected paths according to the specific rules:
* - .aios-core/core/** gets expanded one level deep
* - Subdirs with exceptions inside get expanded further (file-level)
* - Order: regular subdirs first (sorted), then root files (sorted),
* then exception-expanded subdirs (sorted) at the end
* - All other paths stay as-is (no expansion)
*/
function expandProtectedPaths(protectedPaths, exceptions, projectRoot) {
const allPaths = [];
for (const globPath of protectedPaths) {
if (globPath === '.aios-core/core/**') {
const { dirs, files } = expandOneLevel('.aios-core/core', projectRoot);
// Separate dirs into regular (no exceptions inside) and special (has exceptions)
const regularDirs = [];
const specialDirEntries = [];
for (const dir of dirs) {
const relevantExceptions = exceptions.filter(function(exc) {
return isChildOf(exc, dir);
});
if (relevantExceptions.length > 0) {
// Expand this dir further, excluding exceptions
const expanded = expandSubdirWithExceptions(dir, relevantExceptions, projectRoot);
specialDirEntries.push(...expanded);
} else {
regularDirs.push(dir + '/**');
}
}
// Order: regular subdirs, then root files, then special-expanded entries
allPaths.push(...regularDirs);
allPaths.push(...files);
allPaths.push(...specialDirEntries);
} else {
allPaths.push(globPath);
}
}
return allPaths;
}
/**
* Keep exception paths as-is (no expansion needed).
*/
function expandExceptionPaths(exceptionPaths) {
return [...exceptionPaths].sort();
}
/**
* Generate permissions object from boundary config.
*/
function generatePermissions(boundary, projectRoot) {
if (!boundary.frameworkProtection) {
return { deny: [], allow: [] };
}
const denyPaths = expandProtectedPaths(boundary.protected, boundary.exceptions, projectRoot);
const deny = [];
for (const denyPath of denyPaths) {
for (const tool of TOOLS) {
deny.push(tool + '(' + denyPath + ')');
}
}
const allowPaths = expandExceptionPaths(boundary.exceptions);
const allow = [];
for (const allowPath of allowPaths) {
for (const tool of TOOLS) {
allow.push(tool + '(' + allowPath + ')');
}
}
allow.push('Read(.aios-core/**)');
return { deny, allow };
}
/**
* Write settings.json preserving user sections outside permissions.
*/
function writeSettingsJson(projectRoot, permissions) {
const claudeDir = path.join(projectRoot, '.claude');
const fullSettingsPath = path.join(projectRoot, SETTINGS_FILE);
if (!fs.existsSync(claudeDir)) {
fs.mkdirSync(claudeDir, { recursive: true });
}
let existing = {};
if (fs.existsSync(fullSettingsPath)) {
try {
const content = fs.readFileSync(fullSettingsPath, 'utf8');
existing = JSON.parse(content);
} catch {
console.warn('WARNING: Existing settings.json is invalid JSON, starting fresh.');
existing = {};
}
}
const updated = { ...existing };
if (permissions.deny.length > 0 || permissions.allow.length > 0) {
updated.permissions = permissions;
} else {
delete updated.permissions;
}
const newContent = JSON.stringify(updated, null, 2) + '\n';
if (fs.existsSync(fullSettingsPath)) {
const currentContent = fs.readFileSync(fullSettingsPath, 'utf8');
if (currentContent === newContent) {
console.log('PASS: settings.json already up to date, no changes needed.');
return;
}
}
fs.writeFileSync(fullSettingsPath, newContent, 'utf8');
console.log('PASS: settings.json updated with ' + permissions.deny.length + ' deny rules and ' + permissions.allow.length + ' allow rules.');
}
/**
* Main generator entry point.
*/
function generate(projectRoot, configOverride) {
const root = projectRoot || process.cwd();
const resolvedRoot = path.resolve(root);
const boundary = configOverride || readBoundaryConfig(resolvedRoot);
const permissions = generatePermissions(boundary, resolvedRoot);
writeSettingsJson(resolvedRoot, permissions);
}
if (require.main === module) {
const projectRoot = process.argv[2] || process.cwd();
try {
generate(projectRoot);
} catch (error) {
console.error('ERROR: ' + error.message);
process.exit(1);
}
}
module.exports = {
generate,
validateBoundaryPath,
readBoundaryConfig,
expandProtectedPaths,
expandExceptionPaths,
expandOneLevel,
expandSubdirWithExceptions,
generatePermissions,
writeSettingsJson,
};