@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.
@@ -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("/foo/bar", "/foo/bar/baz.ts") => true (parent blocks child)
884
- * pathsConflict("/foo/bar/baz.ts", "/foo/bar") => true (child blocks parent)
885
- * pathsConflict("/foo/bar", "/foo/bar") => true (exact match)
886
- * pathsConflict("/foo/bar", "/foo/baz") => false (siblings)
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
- if (_NerveCenter.pathsConflict(requestedPath, lock.filePath)) {
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, or `REQUIRES_ORCHESTRATION` if someone else is editing.\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- Usage: Provide your `agentId` (e.g., 'cursor-agent'), `filePath` (absolute), 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.",
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: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virsanghavi/axis-server",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "Axis MCP Server CLI",
5
5
  "main": "dist/index.js",
6
6
  "bin": {