@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
JavaScript
/**
* 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 "";
}
}
}