micro-models-agent 0.8.0 → 0.9.0
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/config/config.js +66 -3
- package/dist/config/security.js +1 -1
- package/dist/core/bootstrap.js +10 -0
- package/dist/main.js +15904 -0
- package/dist/modules/security/audit-log.js +8 -0
- package/dist/modules/security/command-validator.js +22 -9
- package/dist/modules/security/encryption.js +22 -2
- package/dist/tools/bash.js +7 -4
- package/dist/tools/grep-tool.js +24 -18
- package/dist/tools/pipeline-run.js +114 -9
- package/dist/tools/read-file.js +6 -1
- package/dist/tools/subagent.js +12 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, appendFileSync } from "fs";
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
|
+
import { globalAuditNotifier } from "./audit-notifier";
|
|
4
5
|
/**
|
|
5
6
|
* Directory and file path for audit logs
|
|
6
7
|
*/
|
|
@@ -27,6 +28,13 @@ export function logAudit(entry) {
|
|
|
27
28
|
// Silently fail if we can't write to audit log
|
|
28
29
|
// This shouldn't break the agent
|
|
29
30
|
}
|
|
31
|
+
// Notify audit notifier if enabled
|
|
32
|
+
try {
|
|
33
|
+
globalAuditNotifier.notify(entry);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Silently fail if notification fails
|
|
37
|
+
}
|
|
30
38
|
}
|
|
31
39
|
/**
|
|
32
40
|
* Log a tool call
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
+
import { DEFAULT_SECURITY_CONFIG } from "../../config/security";
|
|
2
|
+
// Fallback in case DEFAULT_SECURITY_CONFIG is not available
|
|
3
|
+
const FALLBACK_BASH_CONFIG = {
|
|
4
|
+
blacklist: ['rm', 'dd', 'chmod', 'wget', 'curl', 'scp', 'ssh', 'nc', 'netcat'],
|
|
5
|
+
whitelist: [],
|
|
6
|
+
blockDangerousFlags: true,
|
|
7
|
+
dangerousFlags: ['--force', '-rf', '--no-preserve-root'],
|
|
8
|
+
dangerousOperators: ['>', '>>', '2>', '2>>', '|', '&&', '||', ';', '&', '`'],
|
|
9
|
+
logCommands: true,
|
|
10
|
+
};
|
|
11
|
+
export const DEFAULT_BASH_CONFIG = DEFAULT_SECURITY_CONFIG?.bash || FALLBACK_BASH_CONFIG;
|
|
1
12
|
/**
|
|
2
13
|
* Check if a command is allowed based on security configuration
|
|
3
14
|
*/
|
|
4
15
|
export function isCommandAllowed(command, securityConfig) {
|
|
5
16
|
// If no security config, allow everything (backward compatibility)
|
|
6
|
-
|
|
7
|
-
|
|
17
|
+
let config = securityConfig || DEFAULT_BASH_CONFIG;
|
|
18
|
+
// Ensure config has all required fields
|
|
19
|
+
if (!config.blacklist || !Array.isArray(config.blacklist)) {
|
|
20
|
+
config = FALLBACK_BASH_CONFIG;
|
|
8
21
|
}
|
|
9
22
|
const trimmedCommand = command.trim();
|
|
10
23
|
if (!trimmedCommand) {
|
|
@@ -13,8 +26,8 @@ export function isCommandAllowed(command, securityConfig) {
|
|
|
13
26
|
// Extract base command (first word)
|
|
14
27
|
const baseCommand = trimmedCommand.split(/\s+/)[0];
|
|
15
28
|
// Check whitelist first (if non-empty, only whitelisted commands are allowed)
|
|
16
|
-
if (
|
|
17
|
-
if (!
|
|
29
|
+
if (config.whitelist.length > 0) {
|
|
30
|
+
if (!config.whitelist.includes(baseCommand)) {
|
|
18
31
|
return {
|
|
19
32
|
allowed: false,
|
|
20
33
|
reason: `Command "${baseCommand}" is not in the whitelist`,
|
|
@@ -22,15 +35,15 @@ export function isCommandAllowed(command, securityConfig) {
|
|
|
22
35
|
}
|
|
23
36
|
}
|
|
24
37
|
// Check blacklist
|
|
25
|
-
if (
|
|
38
|
+
if (config.blacklist.includes(baseCommand)) {
|
|
26
39
|
return {
|
|
27
40
|
allowed: false,
|
|
28
41
|
reason: `Command "${baseCommand}" is blacklisted`,
|
|
29
42
|
};
|
|
30
43
|
}
|
|
31
44
|
// Check for dangerous operators
|
|
32
|
-
if (
|
|
33
|
-
for (const op of
|
|
45
|
+
if (config.blockDangerousFlags) {
|
|
46
|
+
for (const op of config.dangerousOperators || []) {
|
|
34
47
|
if (command.includes(op)) {
|
|
35
48
|
return {
|
|
36
49
|
allowed: false,
|
|
@@ -40,8 +53,8 @@ export function isCommandAllowed(command, securityConfig) {
|
|
|
40
53
|
}
|
|
41
54
|
}
|
|
42
55
|
// Check for dangerous flags
|
|
43
|
-
if (
|
|
44
|
-
for (const flag of
|
|
56
|
+
if (config.blockDangerousFlags) {
|
|
57
|
+
for (const flag of config.dangerousFlags || []) {
|
|
45
58
|
// Check for flag as whole word or with equals
|
|
46
59
|
const flagPattern = new RegExp(`(?:^|\\s)${flag}(?:\\s|$|=)`);
|
|
47
60
|
if (flagPattern.test(command)) {
|
|
@@ -166,7 +166,17 @@ export function encryptSensitiveFields(obj, key) {
|
|
|
166
166
|
result[fieldName] = encryptString(value, key);
|
|
167
167
|
}
|
|
168
168
|
else if (typeof value === 'object' && value !== null) {
|
|
169
|
-
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
result[fieldName] = value.map(item => typeof item === 'object' && item !== null && !(item instanceof RegExp)
|
|
171
|
+
? encryptSensitiveFields(item, key)
|
|
172
|
+
: item);
|
|
173
|
+
}
|
|
174
|
+
else if (value instanceof RegExp) {
|
|
175
|
+
result[fieldName] = value;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
result[fieldName] = encryptSensitiveFields(value, key);
|
|
179
|
+
}
|
|
170
180
|
}
|
|
171
181
|
else {
|
|
172
182
|
result[fieldName] = value;
|
|
@@ -190,7 +200,17 @@ export function decryptSensitiveFields(obj, key) {
|
|
|
190
200
|
}
|
|
191
201
|
}
|
|
192
202
|
else if (typeof value === 'object' && value !== null) {
|
|
193
|
-
|
|
203
|
+
if (Array.isArray(value)) {
|
|
204
|
+
result[fieldName] = value.map(item => typeof item === 'object' && item !== null && !(item instanceof RegExp)
|
|
205
|
+
? decryptSensitiveFields(item, key)
|
|
206
|
+
: item);
|
|
207
|
+
}
|
|
208
|
+
else if (value instanceof RegExp) {
|
|
209
|
+
result[fieldName] = value;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
result[fieldName] = decryptSensitiveFields(value, key);
|
|
213
|
+
}
|
|
194
214
|
}
|
|
195
215
|
else {
|
|
196
216
|
result[fieldName] = value;
|
package/dist/tools/bash.js
CHANGED
|
@@ -3,6 +3,7 @@ import { platform } from 'os';
|
|
|
3
3
|
import { isCommandAllowed, sanitizeCommandForLog } from '../modules/security/command-validator';
|
|
4
4
|
import { logBashCommand, logSecurityBlock } from '../modules/security/audit-log';
|
|
5
5
|
import { getSessionSecurityConfig } from '../modules/security/session-isolation';
|
|
6
|
+
import { DEFAULT_SECURITY_CONFIG } from '../config/security';
|
|
6
7
|
function adaptCommandForWindows(command) {
|
|
7
8
|
if (platform() !== 'win32')
|
|
8
9
|
return command;
|
|
@@ -32,10 +33,12 @@ export const bashTool = {
|
|
|
32
33
|
const originalCommand = String(args.command);
|
|
33
34
|
const command = adaptCommandForWindows(originalCommand);
|
|
34
35
|
const workdir = args.workdir ? String(args.workdir) : ctx.baseDir;
|
|
35
|
-
// Get session-specific security config
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
// Get session-specific security config with defaults
|
|
37
|
+
const appConfig = ctx.config || {};
|
|
38
|
+
const fullSecurityConfig = ctx.sessionContext
|
|
39
|
+
? getSessionSecurityConfig(appConfig, ctx.sessionContext)
|
|
40
|
+
: appConfig.security || DEFAULT_SECURITY_CONFIG;
|
|
41
|
+
const securityConfig = fullSecurityConfig.bash || DEFAULT_SECURITY_CONFIG.bash;
|
|
39
42
|
const validation = isCommandAllowed(command, securityConfig);
|
|
40
43
|
if (!validation.allowed) {
|
|
41
44
|
// Log security block
|
package/dist/tools/grep-tool.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
1
|
+
import { execFileSync, execSync } from 'child_process';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import { t } from '../i18n/index';
|
|
4
|
-
import {
|
|
5
|
-
import { logSecurityBlock } from '../modules/security/audit-log';
|
|
4
|
+
import { logBashCommand } from '../modules/security/audit-log';
|
|
6
5
|
export const grepTool = {
|
|
7
6
|
name: 'grep',
|
|
8
7
|
description: 'Search file contents using a regular expression. Uses ripgrep (rg) if available, otherwise falls back to grep -r.',
|
|
@@ -19,23 +18,16 @@ export const grepTool = {
|
|
|
19
18
|
handler: async (ctx, args) => {
|
|
20
19
|
const pattern = String(args.pattern);
|
|
21
20
|
const searchPath = args.path ? resolve(ctx.baseDir, String(args.path)) : ctx.baseDir;
|
|
22
|
-
// Build
|
|
23
|
-
|
|
21
|
+
// Build rg arguments as an array to avoid shell interpretation of regex
|
|
22
|
+
// metacharacters like |, (, ) — these are regex patterns, not shell operators.
|
|
23
|
+
const rgArgs = ['-n', '--with-filename', pattern, searchPath];
|
|
24
24
|
if (args.include) {
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
// Security check: validate command (grep/rg should be allowed)
|
|
28
|
-
const securityConfig = ctx.config.security?.bash;
|
|
29
|
-
const validation = isCommandAllowed(cmd, securityConfig);
|
|
30
|
-
if (!validation.allowed) {
|
|
31
|
-
logSecurityBlock(ctx.sessionId, "bash_command", validation.reason || "Command blocked by security policy", sanitizeCommandForLog(cmd));
|
|
32
|
-
return {
|
|
33
|
-
success: false,
|
|
34
|
-
output: `[SECURITY BLOCKED] Command is not allowed: ${validation.reason}`,
|
|
35
|
-
};
|
|
25
|
+
rgArgs.push('-g', String(args.include));
|
|
36
26
|
}
|
|
27
|
+
// Log the search (sanitized) for audit purposes
|
|
28
|
+
logBashCommand(ctx.sessionId, `rg ${rgArgs.join(' ')}`, false, 'grep-tool');
|
|
37
29
|
try {
|
|
38
|
-
const output =
|
|
30
|
+
const output = execFileSync('rg', rgArgs, {
|
|
39
31
|
encoding: 'utf-8',
|
|
40
32
|
maxBuffer: 1024 * 1024,
|
|
41
33
|
cwd: ctx.baseDir,
|
|
@@ -45,7 +37,21 @@ export const grepTool = {
|
|
|
45
37
|
catch (e) {
|
|
46
38
|
if (e.status === 1)
|
|
47
39
|
return { success: true, output: t('file.no_matches') };
|
|
48
|
-
|
|
40
|
+
// Fall back to plain grep -r when rg is not available or fails
|
|
41
|
+
try {
|
|
42
|
+
const cmd = `grep -rn "${pattern.replace(/"/g, '\\"')}" "${searchPath}"`;
|
|
43
|
+
const output = execSync(cmd, {
|
|
44
|
+
encoding: 'utf-8',
|
|
45
|
+
maxBuffer: 1024 * 1024,
|
|
46
|
+
cwd: ctx.baseDir,
|
|
47
|
+
});
|
|
48
|
+
return { success: true, output: output || t('file.no_matches') };
|
|
49
|
+
}
|
|
50
|
+
catch (e2) {
|
|
51
|
+
if (e2.status === 1)
|
|
52
|
+
return { success: true, output: t('file.no_matches') };
|
|
53
|
+
return { success: false, output: t('error.grep_failed', { message: e2.message }) };
|
|
54
|
+
}
|
|
49
55
|
}
|
|
50
56
|
},
|
|
51
57
|
};
|
|
@@ -1,7 +1,56 @@
|
|
|
1
1
|
import { t } from '../i18n/index';
|
|
2
2
|
import { PipelineEngine } from '../modules/pipelines/engine';
|
|
3
3
|
import { PipelineParser } from '../modules/pipelines/parser';
|
|
4
|
+
import { TemplateEngine } from '../modules/pipelines/template';
|
|
5
|
+
import { Agent } from '../core/agent';
|
|
6
|
+
import { ContextManager } from '../modules/context/manager';
|
|
7
|
+
import { PluginManager } from '../modules/plugins/manager';
|
|
8
|
+
import { HallucinationDetector } from '../modules/hallucination/detector';
|
|
9
|
+
import { logSecurityBlock } from '../modules/security/audit-log';
|
|
10
|
+
import { getSessionSecurityConfig } from '../modules/security/session-isolation';
|
|
4
11
|
const engine = new PipelineEngine();
|
|
12
|
+
const MAX_CONCURRENT = 3;
|
|
13
|
+
const MAX_ATTEMPTS = 3;
|
|
14
|
+
async function runStep(ctx, step, params, outputs) {
|
|
15
|
+
const prompt = TemplateEngine.render(step.prompt, params, outputs);
|
|
16
|
+
let lastError = "";
|
|
17
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
18
|
+
try {
|
|
19
|
+
const subContextManager = new ContextManager(ctx.config.contextWindow, ctx.config.contextBudget);
|
|
20
|
+
const subPluginManager = new PluginManager();
|
|
21
|
+
const systemPrompt = {
|
|
22
|
+
content: `You are a pipeline step agent ("${step.agent}"). Complete the given task using the available tools.`,
|
|
23
|
+
priority: "critical",
|
|
24
|
+
essential: true,
|
|
25
|
+
estimatedTokens: 80,
|
|
26
|
+
};
|
|
27
|
+
const subDeps = {
|
|
28
|
+
config: ctx.config,
|
|
29
|
+
llmProvider: ctx.llmProvider,
|
|
30
|
+
toolExecutor: ctx.toolExecutor,
|
|
31
|
+
pluginManager: subPluginManager,
|
|
32
|
+
contextManager: subContextManager,
|
|
33
|
+
hallucinationDetector: new HallucinationDetector(),
|
|
34
|
+
logger: ctx.logger,
|
|
35
|
+
baseDir: ctx.baseDir,
|
|
36
|
+
scope: ctx.scope,
|
|
37
|
+
recursionDepth: (ctx.recursionDepth ?? 0) + 1,
|
|
38
|
+
promptBlocks: [systemPrompt],
|
|
39
|
+
};
|
|
40
|
+
const subAgent = new Agent(subDeps);
|
|
41
|
+
const result = await subAgent.run(prompt);
|
|
42
|
+
if (result.success) {
|
|
43
|
+
return { ok: true, output: result.text };
|
|
44
|
+
}
|
|
45
|
+
lastError = result.error || "no output";
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
lastError = e.message;
|
|
49
|
+
}
|
|
50
|
+
await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
51
|
+
}
|
|
52
|
+
return { ok: false, output: "", error: lastError };
|
|
53
|
+
}
|
|
5
54
|
export const pipelineRunTool = {
|
|
6
55
|
name: 'pipeline_run',
|
|
7
56
|
description: 'Run a named pipeline with YAML definition. Creates a DAG of sub-agents that execute in dependency order.',
|
|
@@ -11,26 +60,82 @@ export const pipelineRunTool = {
|
|
|
11
60
|
properties: {
|
|
12
61
|
name: { type: 'string', description: 'Pipeline name' },
|
|
13
62
|
yaml: { type: 'string', description: 'Pipeline YAML definition with steps' },
|
|
63
|
+
params: { type: 'object', description: 'Template params for {key} placeholders' },
|
|
14
64
|
},
|
|
15
65
|
required: ['name', 'yaml'],
|
|
16
66
|
},
|
|
17
|
-
handler: async (
|
|
67
|
+
handler: async (ctx, args) => {
|
|
18
68
|
const name = String(args.name || '');
|
|
19
69
|
const yaml = String(args.yaml || '');
|
|
70
|
+
const params = args.params || {};
|
|
20
71
|
if (!yaml) {
|
|
21
72
|
return { success: false, output: t('pipeline.invalid') };
|
|
22
73
|
}
|
|
74
|
+
if (!ctx.llmProvider || !ctx.toolExecutor) {
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
output: 'Pipeline cannot run: missing llmProvider or toolExecutor in context',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const securityConfig = ctx.sessionContext
|
|
81
|
+
? getSessionSecurityConfig(ctx.config, ctx.sessionContext)
|
|
82
|
+
: ctx.config.security;
|
|
83
|
+
const maxDepth = securityConfig?.maxRecursionDepth ?? 3;
|
|
84
|
+
const currentDepth = ctx.recursionDepth ?? 0;
|
|
85
|
+
if (currentDepth >= maxDepth) {
|
|
86
|
+
logSecurityBlock(ctx.sessionId, "bash_command", `Maximum recursion depth (${maxDepth}) exceeded`, name);
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
output: `[SECURITY BLOCKED] Maximum pipeline recursion depth (${maxDepth}) exceeded`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
23
92
|
try {
|
|
24
93
|
const pipeline = PipelineParser.parse(yaml);
|
|
94
|
+
engine.reset();
|
|
25
95
|
const order = engine.resolveDependencies(pipeline.steps);
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
96
|
+
const completed = new Set();
|
|
97
|
+
const outputs = {};
|
|
98
|
+
const logs = [];
|
|
99
|
+
const failed = new Set();
|
|
100
|
+
while (completed.size < pipeline.steps.length) {
|
|
101
|
+
const ready = engine
|
|
102
|
+
.getReadySteps(pipeline.steps, completed)
|
|
103
|
+
.filter((s) => !failed.has(s.id));
|
|
104
|
+
if (ready.length === 0) {
|
|
105
|
+
if (failed.size > 0)
|
|
106
|
+
break;
|
|
107
|
+
throw new Error(t('pipeline.circular', { stepId: order.join(', ') }));
|
|
108
|
+
}
|
|
109
|
+
const batch = ready.slice(0, MAX_CONCURRENT);
|
|
110
|
+
const results = await Promise.all(batch.map((step) => runStep(ctx, step, params, outputs)));
|
|
111
|
+
for (let i = 0; i < batch.length; i++) {
|
|
112
|
+
const step = batch[i];
|
|
113
|
+
const result = results[i];
|
|
114
|
+
if (result.ok) {
|
|
115
|
+
completed.add(step.id);
|
|
116
|
+
outputs[step.id] = { output: result.output };
|
|
117
|
+
engine.setStepStatus(step.id, 'done');
|
|
118
|
+
logs.push(` ✓ ${step.id}: ${result.output.split("\n")[0]}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
failed.add(step.id);
|
|
122
|
+
engine.setStepStatus(step.id, 'failed');
|
|
123
|
+
logs.push(` ✗ ${step.id}: ${result.error}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (failed.size > 0)
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (failed.size > 0) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
output: `Pipeline "${pipeline.name}" failed. Executed ${completed.size}/${pipeline.steps.length} steps.\n${logs.join("\n")}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
success: true,
|
|
137
|
+
output: `Pipeline "${pipeline.name}" completed successfully (${completed.size} steps).\n${logs.join("\n")}`,
|
|
138
|
+
};
|
|
34
139
|
}
|
|
35
140
|
catch (e) {
|
|
36
141
|
return { success: false, output: `Pipeline error: ${e.message}` };
|
package/dist/tools/read-file.js
CHANGED
|
@@ -2,6 +2,8 @@ import { readFileSync, existsSync } from "fs";
|
|
|
2
2
|
import { resolve, normalize, extname } from "path";
|
|
3
3
|
import { t } from "../i18n/index";
|
|
4
4
|
import { isPathInScope } from "../modules/security/path-validator";
|
|
5
|
+
import { DEFAULT_SECURITY_CONFIG } from "../config/security";
|
|
6
|
+
import { logSecurityBlock } from "../modules/security/audit-log";
|
|
5
7
|
/** Default number of lines returned when the caller omits `limit`. */
|
|
6
8
|
const DEFAULT_LIMIT = 300;
|
|
7
9
|
export const readFileTool = {
|
|
@@ -25,8 +27,11 @@ export const readFileTool = {
|
|
|
25
27
|
},
|
|
26
28
|
handler: async (ctx, args) => {
|
|
27
29
|
const path = String(args.path);
|
|
28
|
-
const
|
|
30
|
+
const securityPaths = ctx.config?.security?.paths || DEFAULT_SECURITY_CONFIG.paths;
|
|
31
|
+
const scopeCheck = isPathInScope(ctx.baseDir, path, ctx.scope, securityPaths);
|
|
29
32
|
if (!scopeCheck.allowed) {
|
|
33
|
+
const pathStr = path;
|
|
34
|
+
logSecurityBlock(ctx.sessionId || undefined, "file_read", scopeCheck.reason || "Path not allowed", pathStr);
|
|
30
35
|
return {
|
|
31
36
|
success: false,
|
|
32
37
|
output: t("file.path_not_allowed", {
|
package/dist/tools/subagent.js
CHANGED
|
@@ -83,6 +83,18 @@ export const subagentTool = {
|
|
|
83
83
|
output: "Sub-agent cannot run: missing llmProvider or toolExecutor in context",
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
+
// Defensive guard: if the caller requested specific tool tags but none of the
|
|
87
|
+
// registered tools match, the sub-agent would have zero tools and would
|
|
88
|
+
// hallucinate instead of acting. Warn the caller so it can correct the tags.
|
|
89
|
+
if (toolTags && toolTags.length > 0) {
|
|
90
|
+
const matched = ctx.toolExecutor.getToolDefinitions(toolTags);
|
|
91
|
+
if (matched.length === 0) {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
output: `[SECURITY BLOCKED] No tools match the requested tool_tags: [${toolTags.join(", ")}]. Check the subagent tool_tags parameter and retry with valid tags (e.g. "file", "code", "shell", "research").`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
86
98
|
try {
|
|
87
99
|
const subContextManager = new ContextManager(ctx.config.contextWindow, ctx.config.contextBudget);
|
|
88
100
|
const subPluginManager = new PluginManager();
|