aiwg
Version:
Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.
416 lines (356 loc) • 11.6 kB
text/typescript
/**
* FrameworkIsolator - Enforce framework-scoped isolation
*
* Ensures framework-specific resources (agents, commands, memory) are isolated
* from each other while allowing shared resources (requirements, architecture)
* to be accessible across all frameworks.
*
* FID-007 Framework-Scoped Workspaces isolation logic.
*
* @module src/plugin/framework-isolator
* @version 1.0.0
* @since 2025-10-23
*/
import * as fs from 'fs/promises';
import * as path from 'path';
// ===========================
// Interfaces
// ===========================
export interface ValidationResult {
valid: boolean;
errors: ValidationError[];
}
export interface ValidationError {
type: 'contamination' | 'permission' | 'symlink';
path: string;
message: string;
}
export interface ResourceInfo {
source: string;
target?: string;
type?: 'framework-specific' | 'shared';
}
export interface FrameworkSuggestion {
resource: string;
suggestedFramework: string;
confidence: number;
}
export interface CategorizedResources {
frameworkSpecific: string[];
shared: string[];
}
// ===========================
// FrameworkIsolator Class
// ===========================
export class FrameworkIsolator {
private projectRoot: string;
private readonly FRAMEWORK_SPECIFIC = ['agents', 'commands', 'memory', 'context'];
private readonly SHARED_RESOURCES = [
'intake', 'requirements', 'architecture', 'testing', 'deployment',
'security', 'quality', 'risks', 'planning', 'reports', 'working',
'handoffs', 'gates', 'decisions', 'team', 'management'
];
constructor(projectRoot: string) {
this.projectRoot = path.resolve(projectRoot);
}
/**
* Get framework-specific path
*
* @param framework - Framework name
* @param resource - Optional resource path
* @returns Full path to framework resource
*/
getFrameworkPath(framework: string, resource?: string): string {
if (framework === 'shared') {
const basePath = path.join(this.projectRoot, '.aiwg', 'shared');
return resource ? path.join(basePath, resource) : basePath;
}
if (!['claude', 'codex', 'cursor'].includes(framework)) {
throw new Error(`Unknown framework: ${framework}`);
}
const basePath = path.join(this.projectRoot, '.aiwg', framework);
if (resource) {
// Normalize resource path
const normalizedResource = resource.startsWith('/') ? resource.slice(1) : resource;
return path.join(basePath, normalizedResource);
}
return basePath;
}
/**
* Check if resource is shared across frameworks
*
* @param resourcePath - Resource path
* @returns True if resource is shared
*/
isSharedResource(resourcePath: string): boolean {
const firstDir = resourcePath.split('/')[0];
return this.SHARED_RESOURCES.includes(firstDir);
}
/**
* Get framework-specific resources
*
* @param framework - Framework name
* @param resourceType - Resource type (agents, commands, etc.)
* @returns Array of resource paths
*/
async getFrameworkResources(framework: string, resourceType: string): Promise<string[]> {
const frameworkPath = this.getFrameworkPath(framework, resourceType);
try {
const entries = await fs.readdir(frameworkPath);
return entries;
} catch {
return [];
}
}
/**
* Get shared resources
*
* @param resourceType - Resource type
* @param framework - Optional framework name (for access checking)
* @returns Array of resource paths
*/
async getSharedResources(resourceType: string, _framework?: string): Promise<string[]> {
const sharedPath = path.join(this.projectRoot, '.aiwg', 'shared', resourceType);
try {
const entries = await fs.readdir(sharedPath);
return entries;
} catch {
return [];
}
}
/**
* Get framework-specific config
*
* @param framework - Framework name
* @returns Framework configuration
*/
async getFrameworkConfig(framework: string): Promise<any> {
const configPath = this.getFrameworkPath(framework, 'settings.json');
try {
const content = await fs.readFile(configPath, 'utf-8');
return JSON.parse(content);
} catch {
// Fallback for YAML config
const yamlPath = this.getFrameworkPath(framework, 'config.yaml');
try {
const content = await fs.readFile(yamlPath, 'utf-8');
// Simple YAML parsing (for basic cases)
return this.parseSimpleYaml(content);
} catch {
return { framework };
}
}
}
/**
* Validate workspace isolation
*
* @returns Validation result
*/
async validateIsolation(): Promise<ValidationResult> {
const errors: ValidationError[] = [];
// Check for framework-specific resources in shared
const sharedPath = path.join(this.projectRoot, '.aiwg', 'shared');
try {
const entries = await fs.readdir(sharedPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && this.FRAMEWORK_SPECIFIC.includes(entry.name)) {
errors.push({
type: 'contamination',
path: path.join('shared', entry.name),
message: `${entry.name} should not be in shared`
});
}
}
// Check for settings files in shared
const files = entries.filter(e => e.isFile());
for (const file of files) {
if (file.name === 'settings.json' || file.name === 'config.yaml') {
errors.push({
type: 'contamination',
path: path.join('shared', file.name),
message: `${file.name} should not be in shared`
});
}
}
} catch {
// Shared directory doesn't exist
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Check if framework can access resource
*
* @param framework - Framework name
* @param resourcePath - Resource path
* @returns True if access allowed
*/
async canAccess(framework: string, resourcePath: string): Promise<boolean> {
// Shared resources are accessible to all
if (resourcePath.startsWith('shared/')) {
return true;
}
// Framework can access its own resources
if (resourcePath.startsWith(`${framework}/`)) {
return true;
}
return false;
}
/**
* Check if framework can read resource
*
* @param framework - Framework name
* @param resourcePath - Resource path
* @returns True if read allowed
*/
async canRead(framework: string, resourcePath: string): Promise<boolean> {
return this.canAccess(framework, resourcePath);
}
/**
* Check if framework can write to resource
*
* @param framework - Framework name
* @param resourcePath - Resource path
* @returns True if write allowed
*/
async canWrite(framework: string, resourcePath: string): Promise<boolean> {
// Cannot write to other framework's directories
if (resourcePath.startsWith('claude/') ||
resourcePath.startsWith('codex/') ||
resourcePath.startsWith('cursor/')) {
return resourcePath.startsWith(`${framework}/`);
}
// Can write to shared if authorized
if (resourcePath.startsWith('shared/')) {
return true;
}
return false;
}
/**
* Identify framework-specific resources in legacy workspace
*
* @param workspacePath - Workspace path
* @returns Array of framework-specific resources
*/
async identifyFrameworkSpecificResources(workspacePath: string): Promise<ResourceInfo[]> {
const resources: ResourceInfo[] = [];
for (const dir of this.FRAMEWORK_SPECIFIC) {
const dirPath = path.join(workspacePath, dir);
try {
const files = await this.listFilesRecursive(dirPath);
for (const file of files) {
resources.push({
source: path.join(dir, file),
type: 'framework-specific'
});
}
} catch {
// Directory doesn't exist
}
}
return resources;
}
/**
* Identify shared resources in legacy workspace
*
* @param workspacePath - Workspace path
* @returns Array of shared resources
*/
async identifySharedResources(workspacePath: string): Promise<ResourceInfo[]> {
const resources: ResourceInfo[] = [];
for (const dir of this.SHARED_RESOURCES) {
const dirPath = path.join(workspacePath, dir);
try {
const files = await this.listFilesRecursive(dirPath);
for (const file of files) {
resources.push({
source: path.join(dir, file),
target: path.join('shared', dir, file)
});
}
} catch {
// Directory doesn't exist
}
}
return resources;
}
/**
* Categorize all resources
*
* @param workspacePath - Workspace path
* @returns Categorized resources
*/
async categorizeResources(workspacePath: string): Promise<CategorizedResources> {
const frameworkSpecificResources = await this.identifyFrameworkSpecificResources(workspacePath);
const sharedResources = await this.identifySharedResources(workspacePath);
return {
frameworkSpecific: frameworkSpecificResources.map(r => r.source),
shared: sharedResources.map(r => r.source)
};
}
/**
* Suggest target frameworks for resources
*
* @param workspacePath - Workspace path
* @returns Array of framework suggestions
*/
async suggestFrameworkTargets(workspacePath: string): Promise<FrameworkSuggestion[]> {
const suggestions: FrameworkSuggestion[] = [];
// Detect installed frameworks
const { FrameworkDetector } = await import('./framework-detector.js');
const detector = new FrameworkDetector(this.projectRoot);
const frameworks = await detector.detectFrameworks();
if (frameworks.length === 0) {
return suggestions;
}
// Suggest first detected framework for all framework-specific resources
const frameworkSpecific = await this.identifyFrameworkSpecificResources(workspacePath);
const targetFramework = frameworks[0];
for (const resource of frameworkSpecific) {
suggestions.push({
resource: resource.source,
suggestedFramework: targetFramework,
confidence: frameworks.length === 1 ? 1.0 : 0.7
});
}
return suggestions;
}
// ===========================
// Helper Methods
// ===========================
private async listFilesRecursive(dirPath: string): Promise<string[]> {
const files: string[] = [];
const walk = async (currentPath: string, relativePath: string = '') => {
const entries = await fs.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
const relPath = path.join(relativePath, entry.name);
if (entry.isDirectory()) {
await walk(fullPath, relPath);
} else {
files.push(relPath);
}
}
};
await walk(dirPath);
return files;
}
private parseSimpleYaml(content: string): any {
const result: any = {};
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const colonIndex = trimmed.indexOf(':');
if (colonIndex === -1) continue;
const key = trimmed.slice(0, colonIndex).trim();
const value = trimmed.slice(colonIndex + 1).trim();
if (value) {
result[key] = value;
}
}
return result;
}
}