@virsanghavi/axis-server 1.7.2 → 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 +121 -13
- package/package.json +1 -1
package/dist/mcp-server.mjs
CHANGED
|
@@ -284,7 +284,7 @@ var CircuitOpenError = class extends Error {
|
|
|
284
284
|
this.name = "CircuitOpenError";
|
|
285
285
|
}
|
|
286
286
|
};
|
|
287
|
-
var NerveCenter = class {
|
|
287
|
+
var NerveCenter = class _NerveCenter {
|
|
288
288
|
mutex;
|
|
289
289
|
state;
|
|
290
290
|
contextManager;
|
|
@@ -876,9 +876,98 @@ ${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
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Check if two file paths conflict hierarchically.
|
|
924
|
+
* Both paths should be normalized (relative to project root) before comparison.
|
|
925
|
+
* A lock on a directory blocks locks on any file within it, and vice versa.
|
|
926
|
+
* Examples:
|
|
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)
|
|
931
|
+
*/
|
|
932
|
+
static pathsConflict(pathA, pathB) {
|
|
933
|
+
const a = pathA.replace(/\/+$/, "");
|
|
934
|
+
const b = pathB.replace(/\/+$/, "");
|
|
935
|
+
if (a === b) return true;
|
|
936
|
+
if (b.startsWith(a + "/")) return true;
|
|
937
|
+
if (a.startsWith(b + "/")) return true;
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Find any existing lock that conflicts hierarchically with the requested path.
|
|
942
|
+
* Skips locks owned by the same agent and stale locks.
|
|
943
|
+
* All paths are normalized before comparison.
|
|
944
|
+
*/
|
|
945
|
+
findHierarchicalConflict(requestedPath, requestingAgent, locks) {
|
|
946
|
+
const normalizedRequested = _NerveCenter.normalizeLockPath(requestedPath);
|
|
947
|
+
for (const lock of locks) {
|
|
948
|
+
if (lock.agentId === requestingAgent) continue;
|
|
949
|
+
const isStale = Date.now() - lock.timestamp > this.lockTimeout;
|
|
950
|
+
if (isStale) continue;
|
|
951
|
+
const normalizedLock = _NerveCenter.normalizeLockPath(lock.filePath);
|
|
952
|
+
if (_NerveCenter.pathsConflict(normalizedRequested, normalizedLock)) {
|
|
953
|
+
return lock;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
879
958
|
async proposeFileAccess(agentId, filePath, intent, userPrompt) {
|
|
880
959
|
return await this.mutex.runExclusive(async () => {
|
|
881
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
|
+
}
|
|
882
971
|
if (this.contextManager.apiUrl) {
|
|
883
972
|
try {
|
|
884
973
|
const result = await this.callCoordination("locks", "POST", {
|
|
@@ -927,6 +1016,26 @@ ${notepad}`;
|
|
|
927
1016
|
}
|
|
928
1017
|
if (this.useSupabase && this.supabase && this._projectId) {
|
|
929
1018
|
try {
|
|
1019
|
+
const { data: existingLocks } = await this.supabase.from("locks").select("agent_id, file_path, intent, updated_at").eq("project_id", this._projectId);
|
|
1020
|
+
if (existingLocks && existingLocks.length > 0) {
|
|
1021
|
+
const asFileLocks = existingLocks.map((row2) => ({
|
|
1022
|
+
agentId: row2.agent_id,
|
|
1023
|
+
filePath: row2.file_path,
|
|
1024
|
+
intent: row2.intent,
|
|
1025
|
+
userPrompt: "",
|
|
1026
|
+
timestamp: row2.updated_at ? Date.parse(row2.updated_at) : Date.now()
|
|
1027
|
+
}));
|
|
1028
|
+
const conflict2 = this.findHierarchicalConflict(filePath, agentId, asFileLocks);
|
|
1029
|
+
if (conflict2) {
|
|
1030
|
+
logger.info(`[proposeFileAccess] Hierarchical conflict: '${filePath}' overlaps with locked '${conflict2.filePath}' (owner: ${conflict2.agentId})`);
|
|
1031
|
+
this.logLockEvent("BLOCKED", filePath, agentId, conflict2.agentId, intent);
|
|
1032
|
+
return {
|
|
1033
|
+
status: "REQUIRES_ORCHESTRATION",
|
|
1034
|
+
message: `Conflict: '${filePath}' overlaps with '${conflict2.filePath}' locked by '${conflict2.agentId}'`,
|
|
1035
|
+
currentLock: conflict2
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
930
1039
|
const { data, error } = await this.supabase.rpc("try_acquire_lock", {
|
|
931
1040
|
p_project_id: this._projectId,
|
|
932
1041
|
p_file_path: filePath,
|
|
@@ -959,17 +1068,16 @@ ${notepad}`;
|
|
|
959
1068
|
logger.warn("[NerveCenter] Lock RPC failed. Falling back to local.", e);
|
|
960
1069
|
}
|
|
961
1070
|
}
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
}
|
|
1071
|
+
const allLocks = Object.values(this.state.locks);
|
|
1072
|
+
const conflict = this.findHierarchicalConflict(filePath, agentId, allLocks);
|
|
1073
|
+
if (conflict) {
|
|
1074
|
+
logger.info(`[proposeFileAccess] Hierarchical conflict (local): '${filePath}' overlaps with locked '${conflict.filePath}' (owner: ${conflict.agentId})`);
|
|
1075
|
+
this.logLockEvent("BLOCKED", filePath, agentId, conflict.agentId, intent);
|
|
1076
|
+
return {
|
|
1077
|
+
status: "REQUIRES_ORCHESTRATION",
|
|
1078
|
+
message: `Conflict: '${filePath}' overlaps with '${conflict.filePath}' locked by '${conflict.agentId}'`,
|
|
1079
|
+
currentLock: conflict
|
|
1080
|
+
};
|
|
973
1081
|
}
|
|
974
1082
|
this.state.locks[filePath] = { agentId, filePath, intent, userPrompt, timestamp: Date.now() };
|
|
975
1083
|
await this.saveState();
|
|
@@ -1961,7 +2069,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1961
2069
|
// --- Decision & Orchestration ---
|
|
1962
2070
|
{
|
|
1963
2071
|
name: "propose_file_access",
|
|
1964
|
-
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.",
|
|
1965
2073
|
inputSchema: {
|
|
1966
2074
|
type: "object",
|
|
1967
2075
|
properties: {
|