@virsanghavi/axis-server 1.8.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp-server.mjs +63 -6
- package/package.json +1 -1
package/dist/mcp-server.mjs
CHANGED
|
@@ -876,14 +876,58 @@ ${notepad}`;
|
|
|
876
876
|
}
|
|
877
877
|
}
|
|
878
878
|
// --- Decision & Orchestration ---
|
|
879
|
+
/**
|
|
880
|
+
* Normalize a lock path to be relative to the project root.
|
|
881
|
+
* Strips the project root prefix (process.cwd()) so that absolute and relative
|
|
882
|
+
* paths resolve to the same key.
|
|
883
|
+
* Examples (assuming cwd = /Users/vir/Projects/MyApp):
|
|
884
|
+
* "/Users/vir/Projects/MyApp/src/api/v1/route.ts" => "src/api/v1/route.ts"
|
|
885
|
+
* "src/api/v1/route.ts" => "src/api/v1/route.ts"
|
|
886
|
+
* "/Users/vir/Projects/MyApp/" => "" (project root)
|
|
887
|
+
*/
|
|
888
|
+
static normalizeLockPath(filePath) {
|
|
889
|
+
let normalized = filePath.replace(/\/+$/, "");
|
|
890
|
+
const cwd = process.cwd().replace(/\/+$/, "");
|
|
891
|
+
if (normalized.startsWith(cwd + "/")) {
|
|
892
|
+
normalized = normalized.slice(cwd.length + 1);
|
|
893
|
+
} else if (normalized === cwd) {
|
|
894
|
+
normalized = "";
|
|
895
|
+
}
|
|
896
|
+
normalized = normalized.replace(/^\/+/, "");
|
|
897
|
+
return normalized;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Validate that a lock path is not overly broad.
|
|
901
|
+
* Directory locks (last segment has no file extension) must have at least
|
|
902
|
+
* MIN_DIR_LOCK_DEPTH segments from the project root.
|
|
903
|
+
* This prevents agents from locking huge swaths of the codebase like
|
|
904
|
+
* "src/" or "frontend/" which would block every other agent.
|
|
905
|
+
*/
|
|
906
|
+
static MIN_DIR_LOCK_DEPTH = 2;
|
|
907
|
+
static validateLockScope(normalizedPath) {
|
|
908
|
+
if (!normalizedPath || normalizedPath === "." || normalizedPath === "/") {
|
|
909
|
+
return { valid: false, reason: "Cannot lock the entire project root. Lock specific files or subdirectories instead." };
|
|
910
|
+
}
|
|
911
|
+
const segments = normalizedPath.split("/").filter(Boolean);
|
|
912
|
+
const lastSegment = segments[segments.length - 1] || "";
|
|
913
|
+
const hasExtension = lastSegment.includes(".");
|
|
914
|
+
if (!hasExtension && segments.length < _NerveCenter.MIN_DIR_LOCK_DEPTH) {
|
|
915
|
+
return {
|
|
916
|
+
valid: false,
|
|
917
|
+
reason: `Directory lock '${normalizedPath}' is too broad (depth ${segments.length}, minimum ${_NerveCenter.MIN_DIR_LOCK_DEPTH}). Lock a more specific subdirectory or individual files instead.`
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
return { valid: true };
|
|
921
|
+
}
|
|
879
922
|
/**
|
|
880
923
|
* Check if two file paths conflict hierarchically.
|
|
924
|
+
* Both paths should be normalized (relative to project root) before comparison.
|
|
881
925
|
* A lock on a directory blocks locks on any file within it, and vice versa.
|
|
882
926
|
* Examples:
|
|
883
|
-
* pathsConflict("/
|
|
884
|
-
* pathsConflict("/
|
|
885
|
-
* pathsConflict("/
|
|
886
|
-
* pathsConflict("/
|
|
927
|
+
* pathsConflict("src/api", "src/api/route.ts") => true (parent blocks child)
|
|
928
|
+
* pathsConflict("src/api/route.ts", "src/api") => true (child blocks parent)
|
|
929
|
+
* pathsConflict("src/api", "src/api") => true (exact match)
|
|
930
|
+
* pathsConflict("src/api", "src/lib") => false (siblings)
|
|
887
931
|
*/
|
|
888
932
|
static pathsConflict(pathA, pathB) {
|
|
889
933
|
const a = pathA.replace(/\/+$/, "");
|
|
@@ -896,13 +940,16 @@ ${notepad}`;
|
|
|
896
940
|
/**
|
|
897
941
|
* Find any existing lock that conflicts hierarchically with the requested path.
|
|
898
942
|
* Skips locks owned by the same agent and stale locks.
|
|
943
|
+
* All paths are normalized before comparison.
|
|
899
944
|
*/
|
|
900
945
|
findHierarchicalConflict(requestedPath, requestingAgent, locks) {
|
|
946
|
+
const normalizedRequested = _NerveCenter.normalizeLockPath(requestedPath);
|
|
901
947
|
for (const lock of locks) {
|
|
902
948
|
if (lock.agentId === requestingAgent) continue;
|
|
903
949
|
const isStale = Date.now() - lock.timestamp > this.lockTimeout;
|
|
904
950
|
if (isStale) continue;
|
|
905
|
-
|
|
951
|
+
const normalizedLock = _NerveCenter.normalizeLockPath(lock.filePath);
|
|
952
|
+
if (_NerveCenter.pathsConflict(normalizedRequested, normalizedLock)) {
|
|
906
953
|
return lock;
|
|
907
954
|
}
|
|
908
955
|
}
|
|
@@ -911,6 +958,16 @@ ${notepad}`;
|
|
|
911
958
|
async proposeFileAccess(agentId, filePath, intent, userPrompt) {
|
|
912
959
|
return await this.mutex.runExclusive(async () => {
|
|
913
960
|
logger.info(`[proposeFileAccess] Starting - agentId: ${agentId}, filePath: ${filePath}`);
|
|
961
|
+
const normalizedPath = _NerveCenter.normalizeLockPath(filePath);
|
|
962
|
+
logger.info(`[proposeFileAccess] Normalized path: '${normalizedPath}' (from '${filePath}')`);
|
|
963
|
+
const scopeCheck = _NerveCenter.validateLockScope(normalizedPath);
|
|
964
|
+
if (!scopeCheck.valid) {
|
|
965
|
+
logger.warn(`[proposeFileAccess] REJECTED \u2014 scope too broad: ${scopeCheck.reason}`);
|
|
966
|
+
return {
|
|
967
|
+
status: "REJECTED",
|
|
968
|
+
message: scopeCheck.reason
|
|
969
|
+
};
|
|
970
|
+
}
|
|
914
971
|
if (this.contextManager.apiUrl) {
|
|
915
972
|
try {
|
|
916
973
|
const result = await this.callCoordination("locks", "POST", {
|
|
@@ -2012,7 +2069,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2012
2069
|
// --- Decision & Orchestration ---
|
|
2013
2070
|
{
|
|
2014
2071
|
name: "propose_file_access",
|
|
2015
|
-
description: "**CRITICAL: REQUEST FILE LOCK** \u2014 call this before EVERY file edit, no exceptions.\n- Checks if another agent currently holds a lock.\n- Returns `GRANTED` if safe to proceed,
|
|
2072
|
+
description: "**CRITICAL: REQUEST FILE LOCK** \u2014 call this before EVERY file edit, no exceptions.\n- Checks if another agent currently holds a lock.\n- Returns `GRANTED` if safe to proceed, `REQUIRES_ORCHESTRATION` if someone else is editing, or `REJECTED` if the lock scope is too broad.\n- **Hierarchical matching**: Locking a directory also blocks locks on files within it, and vice versa. E.g. locking `src/api/` blocks `src/api/auth/login.ts`.\n- **Scope guard**: Overly broad directory locks are rejected. You cannot lock top-level directories like `src/` or `frontend/` \u2014 lock specific subdirectories (e.g. `src/api/auth/`) or individual files instead.\n- Paths are normalized relative to the project root, so absolute and relative paths are treated equivalently.\n- Usage: Provide your `agentId` (e.g., 'cursor-agent'), `filePath` (absolute or relative), and `intent` (descriptive \u2014 e.g. 'Refactor auth to use JWT', NOT 'editing file').\n- Locks expire after 30 minutes. Use `force_unlock` only as a last resort for crashed agents.\n- **IMPORTANT**: Every lock you acquire MUST be released. Call `complete_job` when done with each task, and `finalize_session` before ending your session. Dangling locks block all other agents.",
|
|
2016
2073
|
inputSchema: {
|
|
2017
2074
|
type: "object",
|
|
2018
2075
|
properties: {
|