micro-models-agent 0.16.2 → 0.16.4

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.
@@ -7,13 +7,6 @@ import { MigrationDetector } from '../migration/detect';
7
7
  import { BackupManager } from '../migration/backup';
8
8
  import { validateExpertConfig } from './experts';
9
9
  import { ConfigEncryptor } from '../modules/security/encryption';
10
- /**
11
- * When the user's config version is older than this, force-overwrite
12
- * the security bash settings with the current defaults.
13
- * This ensures critical security changes (like operator allowlists)
14
- * are applied even if the user has a saved config.
15
- */
16
- const FORCE_SECURITY_UPDATE_VERSION = '2.1.0';
17
10
  /**
18
11
  * Restore RegExp instances in dangerousPatterns that were serialized as {}
19
12
  * (pre-0.8.0 configs) or as {__regex, source, flags} (new format).
@@ -122,42 +115,6 @@ function applyEnvVars(config) {
122
115
  result.moe = { ...result.moe, enabled: env.MMA_MOE_ENABLED === 'true' };
123
116
  return result;
124
117
  }
125
- /**
126
- * Force-update security settings when upgrading from an older version.
127
- * This overrides user config for critical security fields that must
128
- * match the current code defaults (e.g. dangerousOperators).
129
- */
130
- function forceSecurityUpdate(config) {
131
- const userVersion = config.version || '0.0.0';
132
- if (userVersion >= FORCE_SECURITY_UPDATE_VERSION)
133
- return config;
134
- // Force-overwrite bash security with current defaults
135
- const security = config.security || {};
136
- const userBash = security.bash || {};
137
- security.bash = {
138
- ...DEFAULT_SECURITY_CONFIG.bash,
139
- // Keep user's custom blacklist additions, but ensure defaults are present
140
- blacklist: [...new Set([
141
- ...DEFAULT_SECURITY_CONFIG.bash.blacklist,
142
- ...(userBash.blacklist || []),
143
- ])],
144
- whitelist: userBash.whitelist || DEFAULT_SECURITY_CONFIG.bash.whitelist,
145
- dangerousFlags: [...new Set([
146
- ...DEFAULT_SECURITY_CONFIG.bash.dangerousFlags,
147
- ...(userBash.dangerousFlags || []),
148
- ])],
149
- // dangerousOperators: merge user's additions with defaults (user can add but not remove critical ones)
150
- dangerousOperators: [...new Set([
151
- ...DEFAULT_SECURITY_CONFIG.bash.dangerousOperators,
152
- ...(userBash.dangerousOperators || []),
153
- ])],
154
- blockDangerousFlags: DEFAULT_SECURITY_CONFIG.bash.blockDangerousFlags,
155
- logCommands: DEFAULT_SECURITY_CONFIG.bash.logCommands,
156
- };
157
- config.security = security;
158
- console.log(t('migration.security_updated', { from: userVersion, to: FORCE_SECURITY_UPDATE_VERSION }));
159
- return config;
160
- }
161
118
  export function loadConfig(options) {
162
119
  const globalPath = join(options.configDir, 'config.json');
163
120
  mkdirSync(options.configDir, { recursive: true });
@@ -189,8 +146,6 @@ export function loadConfig(options) {
189
146
  if (config.security?.contentScan?.dangerousPatterns) {
190
147
  config.security.contentScan.dangerousPatterns = restoreDangerousPatterns(config.security.contentScan.dangerousPatterns, DEFAULT_SECURITY_CONFIG.contentScan.dangerousPatterns);
191
148
  }
192
- // Force-update security settings when upgrading from older version
193
- config = forceSecurityUpdate(config);
194
149
  config = applyEnvVars(config);
195
150
  // Decrypt sensitive fields in the loaded config
196
151
  try {
@@ -41,6 +41,9 @@ export const DEFAULT_SECURITY_CONFIG = {
41
41
  "mkfs",
42
42
  "mount",
43
43
  "umount",
44
+ "powershell",
45
+ "pwsh",
46
+ "cmd",
44
47
  ],
45
48
  // If whitelist is non-empty, only these commands are allowed
46
49
  whitelist: [],
package/dist/i18n/en.json CHANGED
@@ -419,7 +419,6 @@
419
419
  "migration.dir_bak": "- .mma/ \u2192 .mma.bak/ ({count} files)",
420
420
  "migration.migrated": "Migrated:",
421
421
  "migration.not_needed": "No migration needed",
422
- "migration.security_updated": "[MMA] Security config force-updated from v{from} to v{to} (dangerous operators reset to defaults)",
423
422
  "ui.error_prefix": "Error: ",
424
423
  "ui.success_prefix": "\u2713 ",
425
424
  "ui.warning_prefix": "\u26a0 ",
package/dist/i18n/ru.json CHANGED
@@ -419,7 +419,6 @@
419
419
  "migration.dir_bak": "- .mma/ → .mma.bak/ ({count} файлов)",
420
420
  "migration.migrated": "Мигрировано:",
421
421
  "migration.not_needed": "Миграция не требуется",
422
- "migration.security_updated": "[MMA] Конфигурация безопасности принудительно обновлена с v{from} до v{to} (опасные операторы сброшены)",
423
422
  "ui.error_prefix": "Ошибка: ",
424
423
  "ui.success_prefix": "✓ ",
425
424
  "ui.warning_prefix": "⚠ ",
@@ -97,16 +97,18 @@ export class StuckDetector {
97
97
  if (this.isStuck()) {
98
98
  return t('exec.stuck_recovery', { iterations: this.iterationsOnCurrentStep });
99
99
  }
100
+ // Per-tool error recovery (more specific — e.g., "bash failed 3 times")
101
+ const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
102
+ if (errorTool) {
103
+ return t('exec.tool_errors_recovery', { tool: errorTool[0], count: errorTool[1] });
104
+ }
105
+ // Consecutive failures from different tools (generic — e.g., "5 consecutive failures")
100
106
  if (this.hasConsecutiveFailures()) {
101
107
  return t('exec.consecutive_failures_recovery', { count: this.consecutiveFailures });
102
108
  }
103
109
  if (this.hasRepetitiveToolCalls()) {
104
110
  return this.getRepetitiveToolMessage();
105
111
  }
106
- const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
107
- if (errorTool) {
108
- return t('exec.tool_errors_recovery', { tool: errorTool[0], count: errorTool[1] });
109
- }
110
112
  return '';
111
113
  }
112
114
  checkOffTrack(toolName, call) {
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_SECURITY_CONFIG } from "../../config/security";
2
2
  // Fallback in case DEFAULT_SECURITY_CONFIG is not available
3
3
  const FALLBACK_BASH_CONFIG = {
4
- blacklist: ['rm', 'dd', 'chmod', 'wget', 'curl', 'scp', 'ssh', 'nc', 'netcat'],
4
+ blacklist: ['rm', 'dd', 'chmod', 'wget', 'curl', 'scp', 'ssh', 'nc', 'netcat', 'powershell', 'pwsh', 'cmd'],
5
5
  whitelist: [],
6
6
  blockDangerousFlags: false,
7
7
  dangerousFlags: ['--force', '-rf', '--no-preserve-root'],
@@ -32,8 +32,7 @@ function extractBaseCommand(trimmed) {
32
32
  i++;
33
33
  continue;
34
34
  }
35
- if (tok === "sudo" || tok === "env" || tok === "command" || tok === "exec" || tok === "nohup" ||
36
- tok === "powershell" || tok === "pwsh" || tok === "cmd") {
35
+ if (tok === "sudo" || tok === "env" || tok === "command" || tok === "exec" || tok === "nohup") {
37
36
  i++;
38
37
  continue;
39
38
  }
@@ -5,8 +5,8 @@ import { DEFAULT_SECURITY_CONFIG } from '../config/security';
5
5
  import { runCommand, processRegistry, isLongRunningCommand } from '../modules/processes';
6
6
  import { t } from '../i18n/index';
7
7
  import { platform } from 'os';
8
+ import { MAX_PREVIEW_LINES } from './preview';
8
9
  const BASH_TIMEOUT_MS = 120_000;
9
- const MAX_PREVIEW_LINES = 25;
10
10
  function adaptCommandForWindows(command) {
11
11
  if (platform() !== 'win32')
12
12
  return command;
@@ -1,6 +1,6 @@
1
1
  import { globSync } from 'fs';
2
2
  import { t } from '../i18n/index';
3
- const MAX_PREVIEW_LINES = 25;
3
+ import { MAX_PREVIEW_LINES } from './preview';
4
4
  export const globTool = {
5
5
  name: 'glob',
6
6
  description: `Search for files matching a glob pattern. Shows up to ${MAX_PREVIEW_LINES} results by default. Uses standard glob syntax (e.g., **/*.ts, src/**/*.test.ts).`,
@@ -2,7 +2,7 @@ import { execFileSync, execSync } from 'child_process';
2
2
  import { resolve } from 'path';
3
3
  import { t } from '../i18n/index';
4
4
  import { logBashCommand } from '../modules/security/audit-log';
5
- const MAX_PREVIEW_LINES = 25;
5
+ import { MAX_PREVIEW_LINES } from './preview';
6
6
  function truncateLines(output) {
7
7
  const lines = output.split('\n');
8
8
  if (lines.length <= MAX_PREVIEW_LINES)
@@ -3,7 +3,7 @@ import { resolve } from 'path';
3
3
  import { t } from '../i18n/index';
4
4
  import { isPathInScope } from '../modules/security/path-validator';
5
5
  import { safeResolvePath } from './path-utils';
6
- const MAX_PREVIEW_LINES = 25;
6
+ import { MAX_PREVIEW_LINES } from './preview';
7
7
  export const listDirTool = {
8
8
  name: 'list_dir',
9
9
  description: `List files and directories in a given path. Shows up to ${MAX_PREVIEW_LINES} entries by default.`,
@@ -0,0 +1,2 @@
1
+ /** Maximum number of lines shown in tool output previews. */
2
+ export const MAX_PREVIEW_LINES = 15;
@@ -1,6 +1,7 @@
1
1
  import { processRegistry } from '../modules/processes';
2
2
  import { t } from '../i18n/index';
3
- const DEFAULT_TAIL = 25;
3
+ import { MAX_PREVIEW_LINES } from './preview';
4
+ const DEFAULT_TAIL = MAX_PREVIEW_LINES;
4
5
  export const processLogTool = {
5
6
  name: 'process_log',
6
7
  description: `Show the buffered output of a background process started via bash. Shows the last ${DEFAULT_TAIL} lines by default. Use after starting a dev server to verify it came up without errors, and while it runs to check its state.`,
@@ -5,8 +5,8 @@ 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
7
  import { safeResolvePath } from "./path-utils";
8
- /** Default number of lines returned when the caller omits `limit`. */
9
- const DEFAULT_LIMIT = 25;
8
+ import { MAX_PREVIEW_LINES } from "./preview";
9
+ const DEFAULT_LIMIT = MAX_PREVIEW_LINES;
10
10
  export const readFileTool = {
11
11
  name: "read_file",
12
12
  description: `Read a file from the filesystem. Reads up to ${DEFAULT_LIMIT} lines at a time by default; use offset to page through large files.`,
@@ -2,9 +2,11 @@ import { t } from '../i18n/index';
2
2
  import { isUrlAllowed, sanitizeUrl } from '../modules/security/network-validator';
3
3
  import { logNetworkRequest, logSecurityBlock } from '../modules/security/audit-log';
4
4
  import { getSessionSecurityConfig } from '../modules/security/session-isolation';
5
+ import { MAX_PREVIEW_LINES } from './preview';
6
+ const MAX_CHARS = 3000;
5
7
  export const webBrowseTool = {
6
8
  name: 'web_browse',
7
- description: 'Fetch and read a web page. Returns the page content as plain text.',
9
+ description: `Fetch and read a web page. Returns the page content as plain text (max ${MAX_PREVIEW_LINES} lines / ${MAX_CHARS} chars).`,
8
10
  tags: ['research'],
9
11
  parameters: {
10
12
  type: 'object',
@@ -39,8 +41,12 @@ export const webBrowseTool = {
39
41
  .trim();
40
42
  // Log successful network request
41
43
  logNetworkRequest(ctx.sessionId, sanitizeUrl(url), true, `Status: ${response.status}`);
42
- const maxLen = securityConfig?.maxResponseSize || 8000;
43
- const content = stripped.length > maxLen ? stripped.slice(0, maxLen) + t('file.truncated') : stripped;
44
+ const maxLen = securityConfig?.maxResponseSize || MAX_CHARS;
45
+ let content = stripped.length > maxLen ? stripped.slice(0, maxLen) + t('file.truncated') : stripped;
46
+ const lines = content.split('\n');
47
+ if (lines.length > MAX_PREVIEW_LINES) {
48
+ content = lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
49
+ }
44
50
  return { success: true, output: content || t('file.empty_page') };
45
51
  }
46
52
  catch (err) {
@@ -3,7 +3,8 @@ import { isUrlAllowed, sanitizeUrl } from '../modules/security/network-validator
3
3
  import { logNetworkRequest, logSecurityBlock } from '../modules/security/audit-log';
4
4
  import { getSessionSecurityConfig } from '../modules/security/session-isolation';
5
5
  import { DEFAULT_SECURITY_CONFIG } from '../config/security';
6
- const MAX_CHARS = 15000;
6
+ import { MAX_PREVIEW_LINES } from './preview';
7
+ const MAX_CHARS = 5000;
7
8
  function stripHtml(html) {
8
9
  return html
9
10
  .replace(/<script[\s\S]*?<\/script>/gi, '')
@@ -50,7 +51,16 @@ export const webFetchTool = {
50
51
  // Log successful network request
51
52
  logNetworkRequest(ctx.sessionId, sanitizeUrl(url), true, `Status: ${response.status}`);
52
53
  if (cleaned.length > (securityConfig?.maxResponseSize || MAX_CHARS)) {
53
- return { success: true, output: cleaned.slice(0, securityConfig?.maxResponseSize || MAX_CHARS) + t('file.truncated') };
54
+ let content = cleaned.slice(0, securityConfig?.maxResponseSize || MAX_CHARS) + t('file.truncated');
55
+ const lines = content.split('\n');
56
+ if (lines.length > MAX_PREVIEW_LINES) {
57
+ content = lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
58
+ }
59
+ return { success: true, output: content };
60
+ }
61
+ const lines = cleaned.split('\n');
62
+ if (lines.length > MAX_PREVIEW_LINES) {
63
+ return { success: true, output: lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)` };
54
64
  }
55
65
  return { success: true, output: cleaned || t('file.empty_page') };
56
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "0.16.2",
3
+ "version": "0.16.4",
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": {