UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

129 lines (128 loc) 5.31 kB
/** * Repository policy enforcement for autoresearch. * * Controls which files can be read/written and validates * git operations against the research branch. */ import { execFileSync } from "node:child_process"; import path from "node:path"; import { logger } from "../utils/logger.js"; export class RepoPolicy { config; resolvedMutablePaths; resolvedImmutablePaths; constructor(config) { this.config = config; this.resolvedMutablePaths = config.mutablePaths.map((p) => path.resolve(config.repoPath, p)); this.resolvedImmutablePaths = config.immutablePaths.map((p) => path.resolve(config.repoPath, p)); } /** Returns true if resolved path is inside repoPath (handles prefix collision) */ isInsideRepo(resolved) { const rel = path.relative(this.config.repoPath, resolved); return !rel.startsWith("..") && !path.isAbsolute(rel); } /** Returns true if path is within mutablePaths and NOT in immutablePaths */ isWriteAllowed(filePath) { const resolved = path.resolve(this.config.repoPath, filePath); // Must be inside repoPath if (!this.isInsideRepo(resolved)) { return false; } // Immutable paths always deny, even if they're children of a mutable parent if (this.isProtected(filePath)) { return false; } return this.resolvedMutablePaths.some((mp) => resolved === mp || resolved.startsWith(mp + path.sep)); } /** Returns true if path is in immutablePaths */ isProtected(filePath) { const resolved = path.resolve(this.config.repoPath, filePath); return this.resolvedImmutablePaths.some((ip) => resolved === ip || resolved.startsWith(ip + path.sep)); } /** Returns true if path is readable (mutable, immutable, or program path) */ isReadAllowed(filePath) { const resolved = path.resolve(this.config.repoPath, filePath); if (!this.isInsideRepo(resolved)) { return false; } const programResolved = path.resolve(this.config.repoPath, this.config.programPath); return (this.isWriteAllowed(filePath) || this.isProtected(filePath) || resolved === programResolved); } /** Validates staged files are all in mutablePaths and on the right branch */ async validateCommit(expectedBranch) { const violations = []; const staged = await this.getStagedFiles(); for (const file of staged) { const resolved = path.resolve(this.config.repoPath, file); // Block results files if (file === this.config.resultsPath) { violations.push(`Results file staged: ${file}`); continue; } // Block state files const stateDir = path.dirname(this.config.statePath); if (file === this.config.statePath || (stateDir !== "." && file.startsWith(stateDir + path.sep))) { violations.push(`State file staged: ${file}`); continue; } // Block immutable files (even if under a mutable parent) if (this.resolvedImmutablePaths.some((ip) => resolved === ip || resolved.startsWith(ip + path.sep))) { violations.push(`Immutable file staged: ${file}`); continue; } // Block non-mutable files if (!this.resolvedMutablePaths.some((mp) => resolved === mp || resolved.startsWith(mp + path.sep))) { violations.push(`Non-mutable file staged: ${file}`); } } // Verify branch const currentBranch = this.getCurrentBranch(); if (currentBranch !== expectedBranch) { violations.push(`Wrong branch: expected ${expectedBranch}, got ${currentBranch}`); } return { valid: violations.length === 0, violations }; } /** Returns list of staged file paths. Throws on git failure. */ async getStagedFiles() { const output = execFileSync("git", ["diff", "--cached", "--name-only"], { cwd: this.config.repoPath, encoding: "utf-8", }); return output.trim().split("\n").filter(Boolean); } /** Returns current git branch */ getCurrentBranch() { try { return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: this.config.repoPath, encoding: "utf-8", }).trim(); } catch (err) { logger.warn("[Autoresearch] getCurrentBranch failed", { repoPath: this.config.repoPath, error: err instanceof Error ? err.message : String(err), }); return ""; } } /** Returns short commit hash */ getHeadCommit() { try { return execFileSync("git", ["rev-parse", "--short=7", "HEAD"], { cwd: this.config.repoPath, encoding: "utf-8", }).trim(); } catch (err) { logger.warn("[Autoresearch] getHeadCommit failed", { repoPath: this.config.repoPath, error: err instanceof Error ? err.message : String(err), }); return ""; } } }