@memoryblock/tools 0.1.0-beta → 0.1.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.
- package/README.md +35 -0
- package/package.json +42 -5
- package/src/base.ts +0 -24
- package/src/core/channels.ts +0 -47
- package/src/core/identity.ts +0 -125
- package/src/dev/index.ts +0 -119
- package/src/fs/index.ts +0 -272
- package/src/index.ts +0 -31
- package/src/registry.ts +0 -108
- package/src/sandbox.ts +0 -169
- package/src/shell/index.ts +0 -96
- package/tsconfig.json +0 -10
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @memoryblock/tools
|
|
2
|
+
|
|
3
|
+
Standard tool definitions and schemas for **memoryblock**.
|
|
4
|
+
|
|
5
|
+
This package handles:
|
|
6
|
+
- Standardized tool execution
|
|
7
|
+
- Zod JSON Schema validations
|
|
8
|
+
- Tool argument type coercion
|
|
9
|
+
|
|
10
|
+
## The `memoryblock` Ecosystem
|
|
11
|
+
|
|
12
|
+
**memoryblock** is a highly modular system. Here are the official packages:
|
|
13
|
+
|
|
14
|
+
**The Core**
|
|
15
|
+
* [**memoryblock**](https://www.npmjs.com/package/memoryblock) - The core engine interface and types.
|
|
16
|
+
* [**@memoryblock/daemon**](https://www.npmjs.com/package/@memoryblock/daemon) - Background daemon manager.
|
|
17
|
+
* [**@memoryblock/api**](https://www.npmjs.com/package/@memoryblock/api) - Core REST and WebSocket API server.
|
|
18
|
+
|
|
19
|
+
**Integrations & Tooling**
|
|
20
|
+
* [**@memoryblock/adapters**](https://www.npmjs.com/package/@memoryblock/adapters) - LLM adapters (OpenAI, Anthropic, Bedrock, etc).
|
|
21
|
+
* [**@memoryblock/channels**](https://www.npmjs.com/package/@memoryblock/channels) - Communication channels (CLI, Telegram, Web).
|
|
22
|
+
* [**@memoryblock/tools**](https://www.npmjs.com/package/@memoryblock/tools) - Standard tool definitions and schemas.
|
|
23
|
+
* [**@memoryblock/locale**](https://www.npmjs.com/package/@memoryblock/locale) - Localization strings and formatting.
|
|
24
|
+
* [**@memoryblock/web**](https://www.npmjs.com/package/@memoryblock/web) - Front-end dashboard and Web UI.
|
|
25
|
+
|
|
26
|
+
**Plugins**
|
|
27
|
+
* [**@memoryblock/plugin-installer**](https://www.npmjs.com/package/@memoryblock/plugin-installer) - Plugin installer and registry manager.
|
|
28
|
+
* [**@memoryblock/plugin-agents**](https://www.npmjs.com/package/@memoryblock/plugin-agents) - Secondary AI agents orchestrator.
|
|
29
|
+
* [**@memoryblock/plugin-aws**](https://www.npmjs.com/package/@memoryblock/plugin-aws) - AWS integrations.
|
|
30
|
+
* [**@memoryblock/plugin-fetch-webpage**](https://www.npmjs.com/package/@memoryblock/plugin-fetch-webpage) - Web content fetching and parsing.
|
|
31
|
+
* [**@memoryblock/plugin-web-search**](https://www.npmjs.com/package/@memoryblock/plugin-web-search) - Web search capabilities.
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
package/package.json
CHANGED
|
@@ -1,17 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memoryblock/tools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"description": "Standard tool definitions and schemas for memoryblock.",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/"
|
|
8
|
+
],
|
|
5
9
|
"exports": {
|
|
6
10
|
".": {
|
|
7
11
|
"types": "./dist/index.d.ts",
|
|
8
12
|
"import": "./dist/index.js"
|
|
9
13
|
}
|
|
10
14
|
},
|
|
11
|
-
"dependencies": {
|
|
12
|
-
"memoryblock": "0.1.0-beta"
|
|
13
|
-
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"build": "tsc -p tsconfig.json"
|
|
16
|
-
}
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"memoryblock": "^0.1.2"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"memoryblock",
|
|
23
|
+
"mblk",
|
|
24
|
+
"ai-agent",
|
|
25
|
+
"assistants",
|
|
26
|
+
"agents",
|
|
27
|
+
"automation",
|
|
28
|
+
"multi-agent",
|
|
29
|
+
"agentic-framework",
|
|
30
|
+
"tools",
|
|
31
|
+
"functions",
|
|
32
|
+
"schemas",
|
|
33
|
+
"file",
|
|
34
|
+
"filesystem",
|
|
35
|
+
"command-line",
|
|
36
|
+
"command-line-tools",
|
|
37
|
+
"command-line-functions",
|
|
38
|
+
"command-line-schemas"
|
|
39
|
+
],
|
|
40
|
+
"author": {
|
|
41
|
+
"name": "Ghazi",
|
|
42
|
+
"url": "https://mgks.dev"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/memoryblock-io/memoryblock.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/memoryblock-io/memoryblock/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://memoryblock.io",
|
|
52
|
+
"funding": "https://github.com/sponsors/mgks",
|
|
53
|
+
"license": "MIT"
|
|
17
54
|
}
|
package/src/base.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { ToolDefinition, ToolContext, ToolExecutionResult } from 'memoryblock';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Base Tool interface. All built-in and plugin tools must implement this.
|
|
5
|
-
*/
|
|
6
|
-
export interface Tool {
|
|
7
|
-
readonly definition: ToolDefinition;
|
|
8
|
-
execute(params: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Helper to create a JSON Schema object for tool parameters.
|
|
13
|
-
*/
|
|
14
|
-
export function createSchema(
|
|
15
|
-
properties: Record<string, { type: string; description: string; enum?: string[] }>,
|
|
16
|
-
required: string[] = [],
|
|
17
|
-
): Record<string, unknown> {
|
|
18
|
-
return {
|
|
19
|
-
type: 'object',
|
|
20
|
-
properties,
|
|
21
|
-
required,
|
|
22
|
-
additionalProperties: false,
|
|
23
|
-
};
|
|
24
|
-
}
|
package/src/core/channels.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { ToolExecutionResult, ToolContext } from 'memoryblock';
|
|
2
|
-
import type { Tool } from '../base.js';
|
|
3
|
-
import { createSchema } from '../base.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* send_channel_message tool allows the monitor to proactively dispatch
|
|
7
|
-
* a message to a specific bound channel (e.g. Telegram or CLI), rather
|
|
8
|
-
* than just passively replying to the channel that initiated the turn.
|
|
9
|
-
*/
|
|
10
|
-
export const dispatchMessageTool: Tool = {
|
|
11
|
-
definition: {
|
|
12
|
-
name: 'send_channel_message',
|
|
13
|
-
description: 'Send a message proactively to a specific active channel (e.g., "telegram" or "cli"). Use this if the founder asks you to send them a message somewhere else.',
|
|
14
|
-
parameters: createSchema(
|
|
15
|
-
{
|
|
16
|
-
channel: { type: 'string', description: 'The exact name of the target channel (e.g., "telegram", "cli").' },
|
|
17
|
-
content: { type: 'string', description: 'The message content to send.' },
|
|
18
|
-
},
|
|
19
|
-
['channel', 'content'],
|
|
20
|
-
),
|
|
21
|
-
requiresApproval: false,
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
async execute(params: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
|
|
25
|
-
const target = params.channel as string;
|
|
26
|
-
const content = params.content as string;
|
|
27
|
-
|
|
28
|
-
if (!context.dispatchMessage) {
|
|
29
|
-
return { content: 'Message dispatching is not supported in the current execution context.', isError: true };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
await context.dispatchMessage(target, content);
|
|
34
|
-
return {
|
|
35
|
-
content: `Message successfully dispatched proactively to channel: ${target}`,
|
|
36
|
-
isError: false,
|
|
37
|
-
};
|
|
38
|
-
} catch (err) {
|
|
39
|
-
return {
|
|
40
|
-
content: `Failed to dispatch message to channel '${target}': ${(err as Error).message}`,
|
|
41
|
-
isError: true,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const channelTools = [dispatchMessageTool];
|
package/src/core/identity.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { promises as fsp } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import type { ToolExecutionResult } from 'memoryblock';
|
|
4
|
-
import type { Tool } from '../base.js';
|
|
5
|
-
import { createSchema } from '../base.js';
|
|
6
|
-
|
|
7
|
-
// Resolve workspace root natively without pulling in core directly to avoid cycles
|
|
8
|
-
import { homedir } from 'node:os';
|
|
9
|
-
function getWsRoot(): string {
|
|
10
|
-
const custom = process.env.MEMORYBLOCK_WS_DIR;
|
|
11
|
-
return custom ? custom : join(homedir(), '.memoryblock', 'ws');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// ===== update_monitor_identity =====
|
|
15
|
-
export const updateMonitorIdentityTool: Tool = {
|
|
16
|
-
definition: {
|
|
17
|
-
name: 'update_monitor_identity',
|
|
18
|
-
description: 'Update the monitor name and emoji for this block. This changes how you are identified in the system and UI.',
|
|
19
|
-
parameters: createSchema(
|
|
20
|
-
{
|
|
21
|
-
name: { type: 'string', description: 'Your new chosen name (e.g. "Ana", "Nexus").' },
|
|
22
|
-
emoji: { type: 'string', description: 'A single emoji representing your persona (e.g. "🤖", "🦊").' },
|
|
23
|
-
},
|
|
24
|
-
['name', 'emoji'],
|
|
25
|
-
),
|
|
26
|
-
requiresApproval: true,
|
|
27
|
-
},
|
|
28
|
-
async execute(params, context): Promise<ToolExecutionResult> {
|
|
29
|
-
try {
|
|
30
|
-
// 1. Update config.json
|
|
31
|
-
const configPath = join(context.blockPath, 'config.json');
|
|
32
|
-
const configRaw = await fsp.readFile(configPath, 'utf8');
|
|
33
|
-
const config = JSON.parse(configRaw);
|
|
34
|
-
|
|
35
|
-
config.monitorName = params.name;
|
|
36
|
-
config.monitorEmoji = params.emoji;
|
|
37
|
-
|
|
38
|
-
await fsp.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
39
|
-
|
|
40
|
-
// 2. Update monitor.md to reflect the new identity explicitly
|
|
41
|
-
const monitorPath = join(context.blockPath, 'monitor.md');
|
|
42
|
-
let monitorContent = '';
|
|
43
|
-
try {
|
|
44
|
-
monitorContent = await fsp.readFile(monitorPath, 'utf8');
|
|
45
|
-
} catch {
|
|
46
|
-
// file doesn't exist yet, that's fine
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const header = `# Identity\nName: ${params.name}\nEmoji: ${params.emoji}\n\n`;
|
|
50
|
-
|
|
51
|
-
// Basic replacement logic if it already has an Identity header
|
|
52
|
-
if (monitorContent.includes('# Identity')) {
|
|
53
|
-
monitorContent = monitorContent.replace(/# Identity[\s\S]*?(?=\n#|$)/, header);
|
|
54
|
-
} else {
|
|
55
|
-
monitorContent = header + monitorContent;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
await fsp.writeFile(monitorPath, monitorContent, 'utf8');
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
content: `Successfully updated monitor identity to ${params.emoji} ${params.name}. The system will reflect this change on the next interaction.`,
|
|
62
|
-
isError: false
|
|
63
|
-
};
|
|
64
|
-
} catch (err) {
|
|
65
|
-
return { content: `Failed to update monitor identity: ${(err as Error).message}`, isError: true };
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// ===== update_founder_info =====
|
|
71
|
-
export const updateFounderInfoTool: Tool = {
|
|
72
|
-
definition: {
|
|
73
|
-
name: 'update_founder_info',
|
|
74
|
-
description: 'Update the global founder profile. Use this when the user tells you about themselves (name, work, preferences). This data is globally shared across all your blocks.',
|
|
75
|
-
parameters: createSchema(
|
|
76
|
-
{
|
|
77
|
-
info: { type: 'string', description: 'The new information to append or update about the founder.' },
|
|
78
|
-
mode: { type: 'string', description: 'Either "append" (add new facts) or "rewrite" (completely rewrite the profile).' },
|
|
79
|
-
},
|
|
80
|
-
['info', 'mode'],
|
|
81
|
-
),
|
|
82
|
-
requiresApproval: false,
|
|
83
|
-
},
|
|
84
|
-
async execute(params, _context): Promise<ToolExecutionResult> {
|
|
85
|
-
try {
|
|
86
|
-
const wsRoot = getWsRoot();
|
|
87
|
-
const founderPath = join(wsRoot, 'founder.md');
|
|
88
|
-
|
|
89
|
-
let content = '';
|
|
90
|
-
try {
|
|
91
|
-
content = await fsp.readFile(founderPath, 'utf8');
|
|
92
|
-
} catch {
|
|
93
|
-
// file doesn't exist yet, that's fine
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const newInfo = params.info as string;
|
|
97
|
-
const mode = params.mode as string;
|
|
98
|
-
|
|
99
|
-
if (mode === 'rewrite') {
|
|
100
|
-
content = `# Founder Profile\n\n${newInfo}\n`;
|
|
101
|
-
} else {
|
|
102
|
-
// append intelligently
|
|
103
|
-
if (!content.includes('# Founder Profile')) {
|
|
104
|
-
content = `# Founder Profile\n\n`;
|
|
105
|
-
}
|
|
106
|
-
const timestamp = new Date().toISOString().split('T')[0];
|
|
107
|
-
content += `\n- [${timestamp}]: ${newInfo}`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
await fsp.writeFile(founderPath, content.trim() + '\n', 'utf8');
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
content: `Successfully updated global founder profile at ${founderPath}.`,
|
|
114
|
-
isError: false
|
|
115
|
-
};
|
|
116
|
-
} catch (err) {
|
|
117
|
-
return { content: `Failed to update founder profile: ${(err as Error).message}`, isError: true };
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
export const identityTools: Tool[] = [
|
|
123
|
-
updateMonitorIdentityTool,
|
|
124
|
-
updateFounderInfoTool,
|
|
125
|
-
];
|
package/src/dev/index.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { promisify } from 'node:util';
|
|
3
|
-
import { promises as fsp } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import type { ToolExecutionResult } from 'memoryblock';
|
|
6
|
-
import type { Tool } from '../base.js';
|
|
7
|
-
import { createSchema } from '../base.js';
|
|
8
|
-
|
|
9
|
-
const execFileAsync = promisify(execFile);
|
|
10
|
-
const DEV_TIMEOUT = 120_000; // 2 minutes for builds
|
|
11
|
-
const MAX_OUTPUT = 50_000;
|
|
12
|
-
|
|
13
|
-
/** Find the project root by looking for package.json. */
|
|
14
|
-
async function findProjectRoot(startDir: string): Promise<string> {
|
|
15
|
-
let dir = startDir;
|
|
16
|
-
for (let i = 0; i < 10; i++) {
|
|
17
|
-
try {
|
|
18
|
-
await fsp.access(join(dir, 'package.json'));
|
|
19
|
-
return dir;
|
|
20
|
-
} catch {
|
|
21
|
-
const parent = join(dir, '..');
|
|
22
|
-
if (parent === dir) break;
|
|
23
|
-
dir = parent;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return startDir;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function truncateOutput(output: string): string {
|
|
30
|
-
if (output.length > MAX_OUTPUT) {
|
|
31
|
-
return output.slice(0, MAX_OUTPUT) + `\n...(truncated, ${output.length} total chars)`;
|
|
32
|
-
}
|
|
33
|
-
return output;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function runCommand(command: string, cwd: string): Promise<ToolExecutionResult> {
|
|
37
|
-
try {
|
|
38
|
-
const { stdout, stderr } = await execFileAsync('/bin/sh', ['-c', command], {
|
|
39
|
-
cwd,
|
|
40
|
-
timeout: DEV_TIMEOUT,
|
|
41
|
-
maxBuffer: 2 * 1024 * 1024,
|
|
42
|
-
env: { ...process.env, HOME: process.env.HOME, FORCE_COLOR: '0' },
|
|
43
|
-
});
|
|
44
|
-
let output = '';
|
|
45
|
-
if (stdout) output += stdout;
|
|
46
|
-
if (stderr) output += (output ? '\n--- stderr ---\n' : '') + stderr;
|
|
47
|
-
return { content: truncateOutput(output || '(no output)'), isError: false };
|
|
48
|
-
} catch (err) {
|
|
49
|
-
const e = err as Error & { stdout?: string; stderr?: string };
|
|
50
|
-
let msg = e.message;
|
|
51
|
-
if (e.stdout) msg += '\n' + e.stdout.slice(0, 10_000);
|
|
52
|
-
if (e.stderr) msg += '\n' + e.stderr.slice(0, 10_000);
|
|
53
|
-
return { content: truncateOutput(`Command failed: ${msg}`), isError: true };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ===== run_lint =====
|
|
58
|
-
export const runLintTool: Tool = {
|
|
59
|
-
definition: {
|
|
60
|
-
name: 'run_lint',
|
|
61
|
-
description: 'Run ESLint.',
|
|
62
|
-
parameters: createSchema(
|
|
63
|
-
{ path: { type: 'string', description: 'Target path.' } },
|
|
64
|
-
[],
|
|
65
|
-
),
|
|
66
|
-
requiresApproval: false,
|
|
67
|
-
},
|
|
68
|
-
async execute(params, context): Promise<ToolExecutionResult> {
|
|
69
|
-
const projectRoot = await findProjectRoot(context.workingDir || context.blockPath);
|
|
70
|
-
const target = (params.path as string) || '.';
|
|
71
|
-
return runCommand(`npx eslint ${target} --no-color 2>&1 || true`, projectRoot);
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// ===== run_build =====
|
|
76
|
-
export const runBuildTool: Tool = {
|
|
77
|
-
definition: {
|
|
78
|
-
name: 'run_build',
|
|
79
|
-
description: 'Run build command.',
|
|
80
|
-
parameters: createSchema({}, []),
|
|
81
|
-
requiresApproval: false,
|
|
82
|
-
},
|
|
83
|
-
async execute(_params, context): Promise<ToolExecutionResult> {
|
|
84
|
-
const projectRoot = await findProjectRoot(context.workingDir || context.blockPath);
|
|
85
|
-
// Try pnpm first, fall back to npm
|
|
86
|
-
try {
|
|
87
|
-
await fsp.access(join(projectRoot, 'pnpm-workspace.yaml'));
|
|
88
|
-
return runCommand('pnpm run build 2>&1', projectRoot);
|
|
89
|
-
} catch {
|
|
90
|
-
return runCommand('npm run build 2>&1', projectRoot);
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// ===== run_test =====
|
|
96
|
-
export const runTestTool: Tool = {
|
|
97
|
-
definition: {
|
|
98
|
-
name: 'run_test',
|
|
99
|
-
description: 'Run tests.',
|
|
100
|
-
parameters: createSchema(
|
|
101
|
-
{ filter: { type: 'string', description: 'Test filter.' } },
|
|
102
|
-
[],
|
|
103
|
-
),
|
|
104
|
-
requiresApproval: false,
|
|
105
|
-
},
|
|
106
|
-
async execute(params, context): Promise<ToolExecutionResult> {
|
|
107
|
-
const projectRoot = await findProjectRoot(context.workingDir || context.blockPath);
|
|
108
|
-
const filter = (params.filter as string) || '';
|
|
109
|
-
try {
|
|
110
|
-
await fsp.access(join(projectRoot, 'pnpm-workspace.yaml'));
|
|
111
|
-
return runCommand(`pnpm test ${filter} 2>&1 || true`, projectRoot);
|
|
112
|
-
} catch {
|
|
113
|
-
return runCommand(`npm test ${filter} 2>&1 || true`, projectRoot);
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
/** All dev tools. */
|
|
119
|
-
export const devTools: Tool[] = [runLintTool, runBuildTool, runTestTool];
|
package/src/fs/index.ts
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { promises as fsp } from 'node:fs';
|
|
2
|
-
import { join, resolve, relative, isAbsolute } from 'node:path';
|
|
3
|
-
import { execFile } from 'node:child_process';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
|
-
import type { ToolExecutionResult, ToolContext } from 'memoryblock';
|
|
6
|
-
import type { Tool } from '../base.js';
|
|
7
|
-
import { createSchema } from '../base.js';
|
|
8
|
-
|
|
9
|
-
const execFileAsync = promisify(execFile);
|
|
10
|
-
|
|
11
|
-
// Security: files that must never be read
|
|
12
|
-
const BLOCKED_PATTERNS = ['.env', 'auth.json', '.memoryblock/auth.json'];
|
|
13
|
-
|
|
14
|
-
function isBlockedPath(filePath: string): boolean {
|
|
15
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
16
|
-
return BLOCKED_PATTERNS.some((p) => normalized.endsWith(p) || normalized.includes(`/${p}`));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Resolve a path. Scope is determined by block permissions. */
|
|
20
|
-
function resolvePath(context: ToolContext, targetPath: string): string {
|
|
21
|
-
const base = context.workingDir || context.blockPath;
|
|
22
|
-
const resolved = isAbsolute(targetPath) ? targetPath : resolve(base, targetPath);
|
|
23
|
-
const scope = context.permissions?.scope || 'block';
|
|
24
|
-
|
|
25
|
-
if (scope === 'system') {
|
|
26
|
-
// Unrestricted — still block sensitive files
|
|
27
|
-
return resolved;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Determine allowed root based on scope
|
|
31
|
-
const allowedRoot = scope === 'workspace' && context.workspacePath
|
|
32
|
-
? context.workspacePath
|
|
33
|
-
: context.blockPath;
|
|
34
|
-
|
|
35
|
-
const rel = relative(allowedRoot, resolved);
|
|
36
|
-
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
37
|
-
const label = scope === 'workspace' ? 'workspace' : 'block directory';
|
|
38
|
-
throw new Error(`Access denied: path "${targetPath}" is outside the ${label}. Current scope: ${scope}.`);
|
|
39
|
-
}
|
|
40
|
-
return resolved;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ===== read_file =====
|
|
44
|
-
export const readFileTool: Tool = {
|
|
45
|
-
definition: {
|
|
46
|
-
name: 'read_file',
|
|
47
|
-
description: 'Read the contents of a file.',
|
|
48
|
-
parameters: createSchema(
|
|
49
|
-
{ path: { type: 'string', description: 'Path to the file.' } },
|
|
50
|
-
['path'],
|
|
51
|
-
),
|
|
52
|
-
requiresApproval: false,
|
|
53
|
-
},
|
|
54
|
-
async execute(params, context): Promise<ToolExecutionResult> {
|
|
55
|
-
const filePath = resolvePath(context, params.path as string);
|
|
56
|
-
if (isBlockedPath(filePath)) {
|
|
57
|
-
return { content: 'Access denied: this file is protected.', isError: true };
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
const content = await fsp.readFile(filePath, 'utf-8');
|
|
61
|
-
// Truncate extremely large files to save tokens
|
|
62
|
-
if (content.length > 100_000) {
|
|
63
|
-
return {
|
|
64
|
-
content: content.slice(0, 100_000) + `\n...(truncated, ${content.length} total chars. Use search_files to find specific content.)`,
|
|
65
|
-
isError: false,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
return { content, isError: false };
|
|
69
|
-
} catch (err) {
|
|
70
|
-
return { content: `Failed to read file: ${(err as Error).message}`, isError: true };
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// ===== write_file =====
|
|
76
|
-
export const writeFileTool: Tool = {
|
|
77
|
-
definition: {
|
|
78
|
-
name: 'write_file',
|
|
79
|
-
description: 'Write content to a file. Creates parent directories if needed.',
|
|
80
|
-
parameters: createSchema(
|
|
81
|
-
{
|
|
82
|
-
path: { type: 'string', description: 'Path to the file.' },
|
|
83
|
-
content: { type: 'string', description: 'Content to write.' },
|
|
84
|
-
},
|
|
85
|
-
['path', 'content'],
|
|
86
|
-
),
|
|
87
|
-
requiresApproval: false,
|
|
88
|
-
},
|
|
89
|
-
async execute(params, context): Promise<ToolExecutionResult> {
|
|
90
|
-
const filePath = resolvePath(context, params.path as string);
|
|
91
|
-
if (isBlockedPath(filePath)) {
|
|
92
|
-
return { content: 'Access denied: this file is protected.', isError: true };
|
|
93
|
-
}
|
|
94
|
-
try {
|
|
95
|
-
await fsp.mkdir(join(filePath, '..'), { recursive: true });
|
|
96
|
-
await fsp.writeFile(filePath, params.content as string, 'utf-8');
|
|
97
|
-
return { content: `Written: ${params.path}`, isError: false };
|
|
98
|
-
} catch (err) {
|
|
99
|
-
return { content: `Failed to write: ${(err as Error).message}`, isError: true };
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// ===== list_directory =====
|
|
105
|
-
export const listDirectoryTool: Tool = {
|
|
106
|
-
definition: {
|
|
107
|
-
name: 'list_directory',
|
|
108
|
-
description: 'List files and directories in a path.',
|
|
109
|
-
parameters: createSchema(
|
|
110
|
-
{ path: { type: 'string', description: 'Path to list. Defaults to workspace root.' } },
|
|
111
|
-
[],
|
|
112
|
-
),
|
|
113
|
-
requiresApproval: false,
|
|
114
|
-
},
|
|
115
|
-
async execute(params, context): Promise<ToolExecutionResult> {
|
|
116
|
-
const dirPath = resolvePath(context, (params.path as string) || '.');
|
|
117
|
-
try {
|
|
118
|
-
const entries = await fsp.readdir(dirPath, { withFileTypes: true });
|
|
119
|
-
const listing = entries
|
|
120
|
-
.map((e) => `${e.isDirectory() ? '📁' : '📄'} ${e.name}`)
|
|
121
|
-
.join('\n');
|
|
122
|
-
return { content: listing || '(empty directory)', isError: false };
|
|
123
|
-
} catch (err) {
|
|
124
|
-
return { content: `Failed to list: ${(err as Error).message}`, isError: true };
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// ===== create_directory =====
|
|
130
|
-
export const createDirectoryTool: Tool = {
|
|
131
|
-
definition: {
|
|
132
|
-
name: 'create_directory',
|
|
133
|
-
description: 'Create a directory.',
|
|
134
|
-
parameters: createSchema(
|
|
135
|
-
{ path: { type: 'string', description: 'Path of the directory to create.' } },
|
|
136
|
-
['path'],
|
|
137
|
-
),
|
|
138
|
-
requiresApproval: false,
|
|
139
|
-
},
|
|
140
|
-
async execute(params, context): Promise<ToolExecutionResult> {
|
|
141
|
-
const dirPath = resolvePath(context, params.path as string);
|
|
142
|
-
try {
|
|
143
|
-
await fsp.mkdir(dirPath, { recursive: true });
|
|
144
|
-
return { content: `Created: ${params.path}`, isError: false };
|
|
145
|
-
} catch (err) {
|
|
146
|
-
return { content: `Failed to create: ${(err as Error).message}`, isError: true };
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
// ===== search_files =====
|
|
152
|
-
export const searchFilesTool: Tool = {
|
|
153
|
-
definition: {
|
|
154
|
-
name: 'search_files',
|
|
155
|
-
description: 'Search for text in files using grep. Returns matching lines with file paths and line numbers.',
|
|
156
|
-
parameters: createSchema(
|
|
157
|
-
{
|
|
158
|
-
query: { type: 'string', description: 'Text to search for.' },
|
|
159
|
-
path: { type: 'string', description: 'Directory to search in. Defaults to workspace root.' },
|
|
160
|
-
include: { type: 'string', description: 'File glob pattern to include, e.g. "*.ts".' },
|
|
161
|
-
},
|
|
162
|
-
['query'],
|
|
163
|
-
),
|
|
164
|
-
requiresApproval: false,
|
|
165
|
-
},
|
|
166
|
-
async execute(params, context): Promise<ToolExecutionResult> {
|
|
167
|
-
const searchDir = resolvePath(context, (params.path as string) || '.');
|
|
168
|
-
const query = params.query as string;
|
|
169
|
-
const include = params.include as string | undefined;
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
const args = ['-rnI', '--color=never', '-m', '50'];
|
|
173
|
-
if (include) args.push('--include', include);
|
|
174
|
-
args.push(query, searchDir);
|
|
175
|
-
|
|
176
|
-
const { stdout } = await execFileAsync('grep', args, {
|
|
177
|
-
timeout: 15_000,
|
|
178
|
-
maxBuffer: 512 * 1024,
|
|
179
|
-
});
|
|
180
|
-
const output = stdout.trim();
|
|
181
|
-
if (!output) return { content: 'No matches found.', isError: false };
|
|
182
|
-
// Truncate if too many results
|
|
183
|
-
if (output.length > 20_000) {
|
|
184
|
-
return { content: output.slice(0, 20_000) + '\n...(truncated)', isError: false };
|
|
185
|
-
}
|
|
186
|
-
return { content: output, isError: false };
|
|
187
|
-
} catch (err) {
|
|
188
|
-
const e = err as Error & { code?: number; stdout?: string };
|
|
189
|
-
if (e.code === 1) return { content: 'No matches found.', isError: false };
|
|
190
|
-
return { content: `Search failed: ${e.message}`, isError: true };
|
|
191
|
-
}
|
|
192
|
-
},
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
// ===== replace_in_file =====
|
|
196
|
-
export const replaceInFileTool: Tool = {
|
|
197
|
-
definition: {
|
|
198
|
-
name: 'replace_in_file',
|
|
199
|
-
description: 'Find and replace text in a file.',
|
|
200
|
-
parameters: createSchema(
|
|
201
|
-
{
|
|
202
|
-
path: { type: 'string', description: 'File path.' },
|
|
203
|
-
find: { type: 'string', description: 'Text to find.' },
|
|
204
|
-
replace: { type: 'string', description: 'Replacement text.' },
|
|
205
|
-
all: { type: 'string', description: 'Replace all? "true"/"false".' },
|
|
206
|
-
},
|
|
207
|
-
['path', 'find', 'replace'],
|
|
208
|
-
),
|
|
209
|
-
requiresApproval: false,
|
|
210
|
-
},
|
|
211
|
-
async execute(params, context): Promise<ToolExecutionResult> {
|
|
212
|
-
const filePath = resolvePath(context, params.path as string);
|
|
213
|
-
if (isBlockedPath(filePath)) {
|
|
214
|
-
return { content: 'Access denied: this file is protected.', isError: true };
|
|
215
|
-
}
|
|
216
|
-
try {
|
|
217
|
-
const content = await fsp.readFile(filePath, 'utf-8');
|
|
218
|
-
const find = params.find as string;
|
|
219
|
-
const replace = params.replace as string;
|
|
220
|
-
const replaceAll = (params.all as string) === 'true';
|
|
221
|
-
|
|
222
|
-
if (!content.includes(find)) {
|
|
223
|
-
return { content: `Text not found in ${params.path}. Check exact whitespace/formatting.`, isError: true };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const updated = replaceAll
|
|
227
|
-
? content.split(find).join(replace)
|
|
228
|
-
: content.replace(find, replace);
|
|
229
|
-
|
|
230
|
-
await fsp.writeFile(filePath, updated, 'utf-8');
|
|
231
|
-
const count = replaceAll ? content.split(find).length - 1 : 1;
|
|
232
|
-
return { content: `Replaced ${count} occurrence(s) in ${params.path}`, isError: false };
|
|
233
|
-
} catch (err) {
|
|
234
|
-
return { content: `Failed: ${(err as Error).message}`, isError: true };
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
// ===== file_info =====
|
|
240
|
-
export const fileInfoTool: Tool = {
|
|
241
|
-
definition: {
|
|
242
|
-
name: 'file_info',
|
|
243
|
-
description: 'Get file metadata: size, modified date, type.',
|
|
244
|
-
parameters: createSchema(
|
|
245
|
-
{ path: { type: 'string', description: 'Path to the file or directory.' } },
|
|
246
|
-
['path'],
|
|
247
|
-
),
|
|
248
|
-
requiresApproval: false,
|
|
249
|
-
},
|
|
250
|
-
async execute(params, context): Promise<ToolExecutionResult> {
|
|
251
|
-
const filePath = resolvePath(context, params.path as string);
|
|
252
|
-
try {
|
|
253
|
-
const stat = await fsp.stat(filePath);
|
|
254
|
-
const info = [
|
|
255
|
-
`Path: ${params.path}`,
|
|
256
|
-
`Type: ${stat.isDirectory() ? 'directory' : 'file'}`,
|
|
257
|
-
`Size: ${stat.size} bytes (${(stat.size / 1024).toFixed(1)} KB)`,
|
|
258
|
-
`Modified: ${stat.mtime.toISOString()}`,
|
|
259
|
-
`Created: ${stat.birthtime.toISOString()}`,
|
|
260
|
-
].join('\n');
|
|
261
|
-
return { content: info, isError: false };
|
|
262
|
-
} catch (err) {
|
|
263
|
-
return { content: `Failed: ${(err as Error).message}`, isError: true };
|
|
264
|
-
}
|
|
265
|
-
},
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
/** All built-in FS tools. */
|
|
269
|
-
export const fsTools: Tool[] = [
|
|
270
|
-
readFileTool, writeFileTool, listDirectoryTool, createDirectoryTool,
|
|
271
|
-
searchFilesTool, replaceInFileTool, fileInfoTool,
|
|
272
|
-
];
|
package/src/index.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
export { ToolRegistry } from './registry.js';
|
|
2
|
-
export type { Tool } from './base.js';
|
|
3
|
-
export { createSchema } from './base.js';
|
|
4
|
-
export { fsTools } from './fs/index.js';
|
|
5
|
-
export { shellTools, isSafeCommand } from './shell/index.js';
|
|
6
|
-
export { devTools } from './dev/index.js';
|
|
7
|
-
export { identityTools } from './core/identity.js';
|
|
8
|
-
export { channelTools } from './core/channels.js';
|
|
9
|
-
|
|
10
|
-
import { ToolRegistry } from './registry.js';
|
|
11
|
-
import { fsTools } from './fs/index.js';
|
|
12
|
-
import { shellTools } from './shell/index.js';
|
|
13
|
-
import { devTools } from './dev/index.js';
|
|
14
|
-
import { identityTools } from './core/identity.js';
|
|
15
|
-
import { channelTools } from './core/channels.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Create a ToolRegistry pre-loaded with all built-in tools.
|
|
19
|
-
*/
|
|
20
|
-
export function createDefaultRegistry(): ToolRegistry {
|
|
21
|
-
const registry = new ToolRegistry();
|
|
22
|
-
|
|
23
|
-
// Core built-in tools
|
|
24
|
-
for (const tool of fsTools) registry.register(tool);
|
|
25
|
-
for (const tool of shellTools) registry.register(tool);
|
|
26
|
-
for (const tool of devTools) registry.register(tool);
|
|
27
|
-
for (const tool of identityTools) registry.register(tool);
|
|
28
|
-
for (const tool of channelTools) registry.register(tool);
|
|
29
|
-
|
|
30
|
-
return registry;
|
|
31
|
-
}
|
package/src/registry.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import type { ToolDefinition, ToolContext, ToolExecutionResult } from 'memoryblock';
|
|
2
|
-
import { log } from 'memoryblock';
|
|
3
|
-
import type { Tool } from './base.js';
|
|
4
|
-
import { ToolSandbox } from './sandbox.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Central tool registry. Manages built-in tools and dynamically loaded plugin tools.
|
|
8
|
-
* Implements the LIST_TOOLS_AVAILABLE discovery pattern for token optimization.
|
|
9
|
-
* All execution passes through ToolSandbox for permission enforcement.
|
|
10
|
-
*/
|
|
11
|
-
export class ToolRegistry {
|
|
12
|
-
private tools = new Map<string, Tool>();
|
|
13
|
-
|
|
14
|
-
/** Register a tool. */
|
|
15
|
-
register(tool: Tool): void {
|
|
16
|
-
this.tools.set(tool.definition.name, tool);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Check if a tool exists. */
|
|
20
|
-
has(name: string): boolean {
|
|
21
|
-
return this.tools.has(name);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Get all tool definitions (for LIST_TOOLS_AVAILABLE). */
|
|
25
|
-
listTools(): ToolDefinition[] {
|
|
26
|
-
return Array.from(this.tools.values()).map((t) => t.definition);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Get the LIST_TOOLS_AVAILABLE meta-tool definition.
|
|
31
|
-
* This is the only tool exposed to the LLM on first contact.
|
|
32
|
-
*/
|
|
33
|
-
getDiscoveryTool(): ToolDefinition {
|
|
34
|
-
return {
|
|
35
|
-
name: 'list_tools_available',
|
|
36
|
-
description: 'Discover all tools available to you. Call this first to see your capabilities.',
|
|
37
|
-
parameters: { type: 'object', properties: {}, required: [], additionalProperties: false },
|
|
38
|
-
requiresApproval: false,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Execute a tool by name.
|
|
44
|
-
* All calls pass through ToolSandbox BEFORE execution.
|
|
45
|
-
* Implements graceful degradation — never throws.
|
|
46
|
-
*/
|
|
47
|
-
async execute(
|
|
48
|
-
name: string,
|
|
49
|
-
params: Record<string, unknown>,
|
|
50
|
-
context: ToolContext,
|
|
51
|
-
): Promise<ToolExecutionResult> {
|
|
52
|
-
// Handle the meta-tool
|
|
53
|
-
if (name === 'list_tools_available') {
|
|
54
|
-
const tools = this.listTools();
|
|
55
|
-
const listing = tools.map((t) => `- **${t.name}**: ${t.description}`).join('\n');
|
|
56
|
-
return {
|
|
57
|
-
content: `You have ${tools.length} tools available:\n\n${listing}\n\nCall any tool by name with the required parameters.`,
|
|
58
|
-
isError: false,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const tool = this.tools.get(name);
|
|
63
|
-
if (!tool) {
|
|
64
|
-
// Graceful degradation — never crash
|
|
65
|
-
return {
|
|
66
|
-
content: `Tool "${name}" not found. Use list_tools_available to see available tools.`,
|
|
67
|
-
isError: true,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ===== SANDBOX GATE =====
|
|
72
|
-
// Validate BEFORE execution. If denied, the tool never runs.
|
|
73
|
-
const denied = ToolSandbox.validate(name, params, context);
|
|
74
|
-
if (denied) {
|
|
75
|
-
return denied;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
return await tool.execute(params, context);
|
|
80
|
-
} catch (err) {
|
|
81
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
82
|
-
return {
|
|
83
|
-
content: `Tool "${name}" failed: ${message}`,
|
|
84
|
-
isError: true,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Load plugin tools from a dynamic import path.
|
|
91
|
-
* Uses try/catch for graceful degradation — never crashes if plugin is missing.
|
|
92
|
-
*/
|
|
93
|
-
async loadPlugin(pluginPath: string): Promise<void> {
|
|
94
|
-
try {
|
|
95
|
-
const mod = await import(pluginPath);
|
|
96
|
-
if (mod.tools && Array.isArray(mod.tools)) {
|
|
97
|
-
for (const tool of mod.tools as Tool[]) {
|
|
98
|
-
this.register(tool);
|
|
99
|
-
}
|
|
100
|
-
} else if (mod.default && typeof mod.default === 'object' && 'definition' in mod.default) {
|
|
101
|
-
this.register(mod.default as Tool);
|
|
102
|
-
}
|
|
103
|
-
} catch (err) {
|
|
104
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
105
|
-
log.warn(`Failed to load plugin "${pluginPath}": ${message}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
package/src/sandbox.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ToolSandbox — central enforcement layer for tool execution.
|
|
3
|
-
*
|
|
4
|
-
* Every tool.execute() call passes through this gate.
|
|
5
|
-
* It intercepts params, scans for file paths, validates them against
|
|
6
|
-
* the block's permission scope, and blocks disallowed operations.
|
|
7
|
-
*
|
|
8
|
-
* This class is designed as a drop-in replaceable module. Any future
|
|
9
|
-
* enforcement backend can substitute this class as long as it exposes
|
|
10
|
-
* the same static validate() and validateCommand() interface.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { resolve, relative, isAbsolute } from 'node:path';
|
|
14
|
-
import type { ToolContext, ToolExecutionResult, PermissionsConfig } from 'memoryblock';
|
|
15
|
-
|
|
16
|
-
// Patterns that look like file paths in tool params
|
|
17
|
-
const PATH_PARAM_NAMES = ['path', 'file', 'filePath', 'directory', 'dir', 'target', 'source', 'destination'];
|
|
18
|
-
|
|
19
|
-
// Sensitive files that should never be accessible regardless of scope
|
|
20
|
-
const SENSITIVE_PATTERNS = [
|
|
21
|
-
'auth.json',
|
|
22
|
-
'.env',
|
|
23
|
-
'.memoryblock/auth.json',
|
|
24
|
-
'id_rsa',
|
|
25
|
-
'id_ed25519',
|
|
26
|
-
'.ssh/config',
|
|
27
|
-
'.aws/credentials',
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
// Shell tools that need special handling
|
|
31
|
-
const SHELL_TOOL_NAMES = ['execute_command', 'run_lint', 'run_build', 'run_test'];
|
|
32
|
-
|
|
33
|
-
export class ToolSandbox {
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Validate a tool call BEFORE execution.
|
|
37
|
-
* Returns null if allowed, or an error ToolExecutionResult if denied.
|
|
38
|
-
*/
|
|
39
|
-
static validate(
|
|
40
|
-
toolName: string,
|
|
41
|
-
params: Record<string, unknown>,
|
|
42
|
-
context: ToolContext,
|
|
43
|
-
): ToolExecutionResult | null {
|
|
44
|
-
const perms = context.permissions || { scope: 'block', allowShell: false, allowNetwork: true, maxTimeout: 120_000 };
|
|
45
|
-
|
|
46
|
-
// 1. Shell access check
|
|
47
|
-
if (SHELL_TOOL_NAMES.includes(toolName)) {
|
|
48
|
-
if (!perms.allowShell && perms.scope !== 'system') {
|
|
49
|
-
return {
|
|
50
|
-
content: `Denied: "${toolName}" requires shell access. Current scope: "${perms.scope}". Run \`mblk permissions ${context.blockName} --allow-shell\` to grant access.`,
|
|
51
|
-
isError: true,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 2. Scan all params for file paths and validate
|
|
57
|
-
const pathViolation = ToolSandbox.scanPaths(params, context, perms);
|
|
58
|
-
if (pathViolation) {
|
|
59
|
-
return { content: pathViolation, isError: true };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 3. Check for sensitive file access in any string param
|
|
63
|
-
const sensitiveHit = ToolSandbox.scanSensitive(params);
|
|
64
|
-
if (sensitiveHit) {
|
|
65
|
-
return {
|
|
66
|
-
content: `Denied: access to "${sensitiveHit}" is blocked for security.`,
|
|
67
|
-
isError: true,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return null; // Allowed
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Scan params for file path values and validate against scope.
|
|
76
|
-
*/
|
|
77
|
-
private static scanPaths(
|
|
78
|
-
params: Record<string, unknown>,
|
|
79
|
-
context: ToolContext,
|
|
80
|
-
perms: PermissionsConfig,
|
|
81
|
-
): string | null {
|
|
82
|
-
if (perms.scope === 'system') return null; // No path restrictions
|
|
83
|
-
|
|
84
|
-
for (const [key, value] of Object.entries(params)) {
|
|
85
|
-
if (typeof value !== 'string') continue;
|
|
86
|
-
|
|
87
|
-
// Check named path params
|
|
88
|
-
const isPathParam = PATH_PARAM_NAMES.some(p =>
|
|
89
|
-
key.toLowerCase().includes(p.toLowerCase()),
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
// Also check any string that looks like an absolute path
|
|
93
|
-
const looksLikePath = isPathParam || value.startsWith('/') || value.startsWith('~');
|
|
94
|
-
|
|
95
|
-
if (!looksLikePath) continue;
|
|
96
|
-
|
|
97
|
-
const resolved = isAbsolute(value)
|
|
98
|
-
? value
|
|
99
|
-
: resolve(context.workingDir || context.blockPath, value);
|
|
100
|
-
|
|
101
|
-
const allowedRoot = perms.scope === 'workspace' && context.workspacePath
|
|
102
|
-
? context.workspacePath
|
|
103
|
-
: context.blockPath;
|
|
104
|
-
|
|
105
|
-
const rel = relative(allowedRoot, resolved);
|
|
106
|
-
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
107
|
-
const label = perms.scope === 'workspace' ? 'workspace' : 'block directory';
|
|
108
|
-
return `Denied: "${key}" points to "${value}" which is outside the ${label}. Scope: "${perms.scope}".`;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Check if any string param references a sensitive file.
|
|
117
|
-
*/
|
|
118
|
-
private static scanSensitive(params: Record<string, unknown>): string | null {
|
|
119
|
-
for (const value of Object.values(params)) {
|
|
120
|
-
if (typeof value !== 'string') continue;
|
|
121
|
-
const normalized = value.replace(/\\/g, '/');
|
|
122
|
-
for (const pattern of SENSITIVE_PATTERNS) {
|
|
123
|
-
if (normalized.endsWith(pattern) || normalized.includes(`/${pattern}`)) {
|
|
124
|
-
return pattern;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Scan a shell command string for path traversal attempts.
|
|
133
|
-
* Returns an error message if the command looks like it's escaping scope.
|
|
134
|
-
*/
|
|
135
|
-
static validateCommand(
|
|
136
|
-
command: string,
|
|
137
|
-
context: ToolContext,
|
|
138
|
-
): string | null {
|
|
139
|
-
const perms = context.permissions || { scope: 'block', allowShell: false, allowNetwork: true, maxTimeout: 120_000 };
|
|
140
|
-
if (perms.scope === 'system') return null;
|
|
141
|
-
|
|
142
|
-
// Check for common escape patterns in shell commands
|
|
143
|
-
const escapePatterns = [
|
|
144
|
-
/\bcd\s+\//, // cd /absolute
|
|
145
|
-
/\bcat\s+\//, // cat /etc/passwd
|
|
146
|
-
/\bls\s+\//, // ls / (outside block)
|
|
147
|
-
/>\s*\//, // redirect to absolute path
|
|
148
|
-
/\|\s*tee\s+\//, // pipe to absolute path
|
|
149
|
-
];
|
|
150
|
-
|
|
151
|
-
// Only flag these in block scope — workspace/system allow broader access
|
|
152
|
-
if (perms.scope === 'block') {
|
|
153
|
-
for (const pattern of escapePatterns) {
|
|
154
|
-
if (pattern.test(command)) {
|
|
155
|
-
return `Denied: command appears to access paths outside the block directory. Scope: "block".`;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Check for sensitive file access in any scope
|
|
161
|
-
for (const sensitive of SENSITIVE_PATTERNS) {
|
|
162
|
-
if (command.includes(sensitive)) {
|
|
163
|
-
return `Denied: command references sensitive file "${sensitive}".`;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
}
|
package/src/shell/index.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { promisify } from 'node:util';
|
|
3
|
-
import type { ToolExecutionResult } from 'memoryblock';
|
|
4
|
-
import type { Tool } from '../base.js';
|
|
5
|
-
import { createSchema } from '../base.js';
|
|
6
|
-
|
|
7
|
-
const execFileAsync = promisify(execFile);
|
|
8
|
-
const DEFAULT_TIMEOUT = 120_000; // 2 minutes
|
|
9
|
-
const MAX_OUTPUT = 50_000;
|
|
10
|
-
|
|
11
|
-
// Commands that are safe to auto-execute without approval
|
|
12
|
-
const SAFE_PREFIXES = [
|
|
13
|
-
'ls', 'cat', 'head', 'tail', 'wc', 'find', 'grep', 'which', 'echo', 'pwd',
|
|
14
|
-
'node --version', 'bun --version', 'pnpm --version', 'npm --version',
|
|
15
|
-
'git status', 'git log', 'git diff', 'git branch',
|
|
16
|
-
'tsc --noEmit', 'npx eslint', 'pnpm lint', 'npm run lint',
|
|
17
|
-
'pnpm build', 'npm run build', 'pnpm test', 'npm test',
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
function isSafeCommand(command: string): boolean {
|
|
21
|
-
const trimmed = command.trim();
|
|
22
|
-
return SAFE_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ===== execute_command =====
|
|
26
|
-
export const executeCommandTool: Tool = {
|
|
27
|
-
definition: {
|
|
28
|
-
name: 'execute_command',
|
|
29
|
-
description: 'Run shell command (Safe cmds run auto).',
|
|
30
|
-
parameters: createSchema(
|
|
31
|
-
{
|
|
32
|
-
command: { type: 'string', description: 'Command.' },
|
|
33
|
-
timeout: { type: 'string', description: 'Timeout (ms).' },
|
|
34
|
-
},
|
|
35
|
-
['command'],
|
|
36
|
-
),
|
|
37
|
-
// Dynamic approval: overridden at dispatch time based on command safety
|
|
38
|
-
requiresApproval: true,
|
|
39
|
-
},
|
|
40
|
-
async execute(params, context): Promise<ToolExecutionResult> {
|
|
41
|
-
const command = params.command as string;
|
|
42
|
-
const scope = context.permissions?.scope || 'block';
|
|
43
|
-
|
|
44
|
-
// Permission check: shell access must be explicitly granted
|
|
45
|
-
if (!context.permissions?.allowShell && scope !== 'system') {
|
|
46
|
-
return {
|
|
47
|
-
content: `Shell access denied. Current permission scope: "${scope}". Set allowShell: true or scope: "system" via \`mblk permissions ${context.blockName} --allow-shell\`.`,
|
|
48
|
-
isError: true,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const timeout = context.permissions?.maxTimeout
|
|
53
|
-
|| (params.timeout ? parseInt(params.timeout as string, 10) : DEFAULT_TIMEOUT);
|
|
54
|
-
|
|
55
|
-
// Determine cwd based on scope
|
|
56
|
-
let cwd = context.workingDir || context.blockPath;
|
|
57
|
-
if (scope === 'block') {
|
|
58
|
-
cwd = context.blockPath;
|
|
59
|
-
} else if (scope === 'workspace' && context.workspacePath) {
|
|
60
|
-
// Allow commands within workspace, but default cwd to block
|
|
61
|
-
cwd = context.workingDir || context.blockPath;
|
|
62
|
-
}
|
|
63
|
-
// scope === 'system' — use whatever workingDir is set
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const { stdout, stderr } = await execFileAsync('/bin/sh', ['-c', command], {
|
|
67
|
-
cwd,
|
|
68
|
-
timeout,
|
|
69
|
-
maxBuffer: 2 * 1024 * 1024,
|
|
70
|
-
env: { ...process.env, HOME: process.env.HOME },
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
let output = '';
|
|
74
|
-
if (stdout) output += stdout;
|
|
75
|
-
if (stderr) output += (output ? '\n--- stderr ---\n' : '') + stderr;
|
|
76
|
-
|
|
77
|
-
if (output.length > MAX_OUTPUT) {
|
|
78
|
-
output = output.slice(0, MAX_OUTPUT) + `\n...(truncated, ${output.length} total chars)`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return { content: output || '(no output)', isError: false };
|
|
82
|
-
} catch (err) {
|
|
83
|
-
const error = err as Error & { stdout?: string; stderr?: string; code?: number };
|
|
84
|
-
let message = error.message;
|
|
85
|
-
if (error.stdout) message += `\nstdout: ${error.stdout.slice(0, 5000)}`;
|
|
86
|
-
if (error.stderr) message += `\nstderr: ${error.stderr.slice(0, 5000)}`;
|
|
87
|
-
return { content: `Command failed: ${message}`, isError: true };
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
/** Check if a command is safe (export for use in approval logic). */
|
|
93
|
-
export { isSafeCommand };
|
|
94
|
-
|
|
95
|
-
/** All built-in shell tools. */
|
|
96
|
-
export const shellTools: Tool[] = [executeCommandTool];
|