micro-models-agent 0.13.0 → 0.13.2

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.
@@ -1,9 +1,9 @@
1
1
  import { mkdirSync, existsSync } from "fs";
2
- import { resolve, normalize } from "path";
3
2
  import { t } from "../i18n/index";
4
3
  import { isPathWritable } from "../modules/security/path-validator";
5
4
  import { logFileWrite, logSecurityBlock } from "../modules/security/audit-log";
6
5
  import { getSessionSecurityConfig } from "../modules/security/session-isolation";
6
+ import { safeResolvePath } from "./path-utils";
7
7
  export const createDirTool = {
8
8
  name: "create_dir",
9
9
  description: "Create a directory (and any intermediate directories).",
@@ -17,8 +17,7 @@ export const createDirTool = {
17
17
  },
18
18
  handler: async (ctx, args) => {
19
19
  const path = String(args.path);
20
- const baseDir = resolve(ctx.baseDir);
21
- const resolved = resolve(baseDir, normalize(path));
20
+ const resolved = safeResolvePath(ctx.baseDir, path);
22
21
  // Get session-specific security config
23
22
  const securityConfig = ctx.sessionContext
24
23
  ? getSessionSecurityConfig(ctx.config, ctx.sessionContext)
@@ -34,7 +33,7 @@ export const createDirTool = {
34
33
  };
35
34
  }
36
35
  // Check path permissions
37
- const scopeCheck = isPathWritable(ctx.baseDir, path, ctx.scope, ctx.config.security?.paths);
36
+ const scopeCheck = isPathWritable(ctx.baseDir, resolved, ctx.scope, ctx.config.security?.paths);
38
37
  if (!scopeCheck.allowed) {
39
38
  logSecurityBlock(ctx.sessionId, "file_write", scopeCheck.reason || "Path not allowed", path);
40
39
  return {
@@ -1,10 +1,10 @@
1
1
  import { unlinkSync, existsSync, statSync, readFileSync } from "fs";
2
- import { resolve, normalize } from "path";
3
2
  import { t } from "../i18n/index";
4
3
  import { isPathWritable } from "../modules/security/path-validator";
5
4
  import { logFileDelete, logSecurityBlock } from "../modules/security/audit-log";
6
5
  import { getSessionSecurityConfig } from "../modules/security/session-isolation";
7
6
  import { generateDeleteDiff } from "../ui/diff";
7
+ import { safeResolvePath } from "./path-utils";
8
8
  export const deleteFileTool = {
9
9
  name: "delete_file",
10
10
  description: "Delete a file from the filesystem.",
@@ -18,8 +18,7 @@ export const deleteFileTool = {
18
18
  },
19
19
  handler: async (ctx, args) => {
20
20
  const path = String(args.path);
21
- const baseDir = resolve(ctx.baseDir);
22
- const resolved = resolve(baseDir, normalize(path));
21
+ const resolved = safeResolvePath(ctx.baseDir, path);
23
22
  // Get session-specific security config
24
23
  const securityConfig = ctx.sessionContext
25
24
  ? getSessionSecurityConfig(ctx.config, ctx.sessionContext)
@@ -35,7 +34,7 @@ export const deleteFileTool = {
35
34
  };
36
35
  }
37
36
  // Check path permissions
38
- const scopeCheck = isPathWritable(ctx.baseDir, path, ctx.scope, ctx.config.security?.paths);
37
+ const scopeCheck = isPathWritable(ctx.baseDir, resolved, ctx.scope, ctx.config.security?.paths);
39
38
  if (!scopeCheck.allowed) {
40
39
  logSecurityBlock(ctx.sessionId, "file_delete", scopeCheck.reason || "Path not allowed", path);
41
40
  return {
@@ -1,11 +1,11 @@
1
1
  import { readFileSync, writeFileSync } from "fs";
2
- import { resolve, normalize } from "path";
3
2
  import { t } from "../i18n/index";
4
3
  import { isPathWritable } from "../modules/security/path-validator";
5
4
  import { scanContent } from "../modules/security/content-scanner";
6
5
  import { logFileWrite, logSecurityBlock } from "../modules/security/audit-log";
7
6
  import { getSessionSecurityConfig } from "../modules/security/session-isolation";
8
7
  import { generateDiff } from "../ui/diff";
8
+ import { safeResolvePath } from "./path-utils";
9
9
  export const editFileTool = {
10
10
  name: "edit_file",
11
11
  description: "Find and replace text in an existing file. Uses exact string match (not regex).",
@@ -21,8 +21,7 @@ export const editFileTool = {
21
21
  },
22
22
  handler: async (ctx, args) => {
23
23
  const path = String(args.path);
24
- const baseDir = resolve(ctx.baseDir);
25
- const resolved = resolve(baseDir, normalize(path));
24
+ const resolved = safeResolvePath(ctx.baseDir, path);
26
25
  // Get session-specific security config
27
26
  const securityConfig = ctx.sessionContext
28
27
  ? getSessionSecurityConfig(ctx.config, ctx.sessionContext)
@@ -38,7 +37,7 @@ export const editFileTool = {
38
37
  };
39
38
  }
40
39
  // Check path permissions
41
- const scopeCheck = isPathWritable(ctx.baseDir, path, ctx.scope, ctx.config.security?.paths);
40
+ const scopeCheck = isPathWritable(ctx.baseDir, resolved, ctx.scope, ctx.config.security?.paths);
42
41
  if (!scopeCheck.allowed) {
43
42
  logSecurityBlock(ctx.sessionId, "file_write", scopeCheck.reason || "Path not allowed", path);
44
43
  return {
@@ -86,4 +86,10 @@ export class ToolExecutor {
86
86
  getToolDefinitions(tags) {
87
87
  return this.registry.getAllForLLM(tags);
88
88
  }
89
+ getRegistry() {
90
+ return this.registry;
91
+ }
92
+ setScope(scope) {
93
+ this.ctx.scope = scope;
94
+ }
89
95
  }
@@ -1,7 +1,7 @@
1
1
  import { statSync, existsSync } from 'fs';
2
- import { resolve, normalize } from 'path';
3
2
  import { t } from '../i18n/index';
4
3
  import { isPathInScope } from '../modules/security/path-validator';
4
+ import { safeResolvePath } from './path-utils';
5
5
  export const fileInfoTool = {
6
6
  name: 'file_info',
7
7
  description: 'Get metadata about a file or directory (size, creation date, modification date).',
@@ -15,10 +15,9 @@ export const fileInfoTool = {
15
15
  },
16
16
  handler: async (ctx, args) => {
17
17
  const path = String(args.path);
18
- const baseDir = resolve(ctx.baseDir);
19
- const resolved = resolve(baseDir, normalize(path));
18
+ const resolved = safeResolvePath(ctx.baseDir, path);
20
19
  // Check path permissions
21
- const scopeCheck = isPathInScope(ctx.baseDir, path, ctx.scope, ctx.config.security?.paths);
20
+ const scopeCheck = isPathInScope(ctx.baseDir, resolved, ctx.scope, ctx.config.security?.paths);
22
21
  if (!scopeCheck.allowed) {
23
22
  return {
24
23
  success: false,
@@ -1,7 +1,8 @@
1
1
  import { readdirSync, statSync, existsSync } from 'fs';
2
- import { resolve, normalize } from 'path';
2
+ import { resolve } from 'path';
3
3
  import { t } from '../i18n/index';
4
4
  import { isPathInScope } from '../modules/security/path-validator';
5
+ import { safeResolvePath } from './path-utils';
5
6
  export const listDirTool = {
6
7
  name: 'list_dir',
7
8
  description: 'List files and directories in a given path.',
@@ -15,10 +16,9 @@ export const listDirTool = {
15
16
  },
16
17
  handler: async (ctx, args) => {
17
18
  const path = String(args.path);
18
- const baseDir = resolve(ctx.baseDir);
19
- const resolved = resolve(baseDir, normalize(path));
19
+ const resolved = safeResolvePath(ctx.baseDir, path);
20
20
  // Check path permissions
21
- const scopeCheck = isPathInScope(ctx.baseDir, path, ctx.scope, ctx.config.security?.paths);
21
+ const scopeCheck = isPathInScope(ctx.baseDir, resolved, ctx.scope, ctx.config.security?.paths);
22
22
  if (!scopeCheck.allowed) {
23
23
  return {
24
24
  success: false,
@@ -0,0 +1,51 @@
1
+ import { resolve, normalize, dirname, basename, sep } from 'path';
2
+ import { existsSync } from 'fs';
3
+ /**
4
+ * Resolve a user-provided path against baseDir.
5
+ *
6
+ * Handles common model mistakes:
7
+ * 1. Leading slash: "/testing/cat3" → resolve relative to baseDir
8
+ * 2. Missing separator: "E:\agent_test" when baseDir is "E:\agent\_test"
9
+ * → inserts the missing backslash
10
+ *
11
+ * For new files the parent directory must exist; we check that instead of
12
+ * the full path so write_file still works.
13
+ */
14
+ export function safeResolvePath(baseDir, userPath) {
15
+ const norm = normalize(userPath);
16
+ const stripped = norm.replace(/^[/\\]/, '');
17
+ const resolved = resolve(baseDir, stripped);
18
+ // Fast path: if it exists (or parent does for new files), return it
19
+ if (existsSync(resolved) || existsSync(dirname(resolved)))
20
+ return resolved;
21
+ const baseNorm = normalize(baseDir);
22
+ // Walk ancestors and look for missing separators
23
+ let cur = baseNorm;
24
+ while (cur && cur !== dirname(cur)) {
25
+ const name = basename(cur);
26
+ if (!name) {
27
+ cur = dirname(cur);
28
+ continue;
29
+ }
30
+ let idx = stripped.toLowerCase().indexOf(name.toLowerCase());
31
+ while (idx >= 0) {
32
+ const afterIdx = idx + name.length;
33
+ const afterChar = stripped[afterIdx];
34
+ if (afterChar && afterChar !== '\\' && afterChar !== '/') {
35
+ const fixed = stripped.slice(0, afterIdx) + sep + stripped.slice(afterIdx);
36
+ const fixedResolved = resolve(baseDir, normalize(fixed));
37
+ if (existsSync(fixedResolved) || existsSync(dirname(fixedResolved))) {
38
+ return fixedResolved;
39
+ }
40
+ // Try from ancestor's parent
41
+ const fromParent = resolve(dirname(cur), normalize(fixed));
42
+ if (existsSync(fromParent) || existsSync(dirname(fromParent))) {
43
+ return fromParent;
44
+ }
45
+ }
46
+ idx = stripped.toLowerCase().indexOf(name.toLowerCase(), idx + 1);
47
+ }
48
+ cur = dirname(cur);
49
+ }
50
+ return resolved;
51
+ }
@@ -1,9 +1,10 @@
1
1
  import { readFileSync, existsSync } from "fs";
2
- import { resolve, normalize, extname } from "path";
2
+ import { extname } from "path";
3
3
  import { t } from "../i18n/index";
4
4
  import { isPathInScope } from "../modules/security/path-validator";
5
5
  import { DEFAULT_SECURITY_CONFIG } from "../config/security";
6
6
  import { logSecurityBlock } from "../modules/security/audit-log";
7
+ import { safeResolvePath } from "./path-utils";
7
8
  /** Default number of lines returned when the caller omits `limit`. */
8
9
  const DEFAULT_LIMIT = 50;
9
10
  export const readFileTool = {
@@ -27,8 +28,9 @@ export const readFileTool = {
27
28
  },
28
29
  handler: async (ctx, args) => {
29
30
  const path = String(args.path);
31
+ const resolved = safeResolvePath(ctx.baseDir, path);
30
32
  const securityPaths = ctx.config?.security?.paths || DEFAULT_SECURITY_CONFIG.paths;
31
- const scopeCheck = isPathInScope(ctx.baseDir, path, ctx.scope, securityPaths);
33
+ const scopeCheck = isPathInScope(ctx.baseDir, resolved, ctx.scope, securityPaths);
32
34
  if (!scopeCheck.allowed) {
33
35
  const pathStr = path;
34
36
  logSecurityBlock(ctx.sessionId || undefined, "file_read", scopeCheck.reason || "Path not allowed", pathStr);
@@ -39,12 +41,13 @@ export const readFileTool = {
39
41
  }),
40
42
  };
41
43
  }
42
- const resolved = resolve(ctx.baseDir, normalize(path));
43
44
  if (!existsSync(resolved)) {
44
45
  return { success: false, output: t("file.notfound", { path }) };
45
46
  }
46
47
  ctx.trackReadPath?.(path);
47
48
  const content = readFileSync(resolved, "utf-8");
49
+ // Track file paths mentioned in the document (e.g. structure.md, README)
50
+ ctx.trackDocumentContent?.(content);
48
51
  const lines = content.split("\n");
49
52
  const total = lines.length;
50
53
  const offset = args.offset || 1;
@@ -1,11 +1,12 @@
1
1
  import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
2
- import { resolve, normalize, dirname } from "path";
2
+ import { dirname } from "path";
3
3
  import { t } from "../i18n/index";
4
4
  import { isPathWritable } from "../modules/security/path-validator";
5
5
  import { scanContent } from "../modules/security/content-scanner";
6
6
  import { logFileWrite, logSecurityBlock } from "../modules/security/audit-log";
7
7
  import { getSessionSecurityConfig } from "../modules/security/session-isolation";
8
8
  import { generateDiff, generateNewFileDiff } from "../ui/diff";
9
+ import { safeResolvePath } from "./path-utils";
9
10
  export const writeFileTool = {
10
11
  name: "write_file",
11
12
  description: "Create or overwrite a file with given content. Creates intermediate directories if needed.",
@@ -20,10 +21,9 @@ export const writeFileTool = {
20
21
  },
21
22
  handler: async (ctx, args) => {
22
23
  const path = String(args.path);
23
- const baseDir = resolve(ctx.baseDir);
24
- const resolved = resolve(baseDir, normalize(path));
25
- // Check path permissions
26
- const scopeCheck = isPathWritable(ctx.baseDir, path, ctx.scope, ctx.config.security?.paths);
24
+ const resolved = safeResolvePath(ctx.baseDir, path);
25
+ // Check path permissions using the resolved path
26
+ const scopeCheck = isPathWritable(ctx.baseDir, resolved, ctx.scope, ctx.config.security?.paths);
27
27
  if (!scopeCheck.allowed) {
28
28
  logSecurityBlock(ctx.sessionId, "file_write", scopeCheck.reason || "Path not allowed", path);
29
29
  return {
@@ -136,6 +136,6 @@ export class Renderer {
136
136
  if (!this.card)
137
137
  return;
138
138
  const { tool, start } = this.card;
139
- this.toolEnd(tool, Date.now() - start, true);
139
+ this.toolEnd(tool, Date.now() - start);
140
140
  }
141
141
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Micro Models Agent (MMA) — LLM agent harness for small models (Qwen3.5-9B, 32K-64K context)",
5
5
  "type": "module",
6
6
  "bin": {