@memoryblock/tools 0.1.0-beta
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/LICENSE +21 -0
- package/dist/base.d.ts +17 -0
- package/dist/base.d.ts.map +1 -0
- package/dist/base.js +12 -0
- package/dist/base.js.map +1 -0
- package/dist/core/channels.d.ts +9 -0
- package/dist/core/channels.d.ts.map +1 -0
- package/dist/core/channels.js +39 -0
- package/dist/core/channels.js.map +1 -0
- package/dist/core/identity.d.ts +5 -0
- package/dist/core/identity.d.ts.map +1 -0
- package/dist/core/identity.js +108 -0
- package/dist/core/identity.js.map +1 -0
- package/dist/dev/index.d.ts +7 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +113 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/fs/index.d.ts +11 -0
- package/dist/fs/index.d.ts.map +1 -0
- package/dist/fs/index.js +240 -0
- package/dist/fs/index.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +33 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +96 -0
- package/dist/registry.js.map +1 -0
- package/dist/sandbox.d.ts +33 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +136 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/shell/index.d.ts +8 -0
- package/dist/shell/index.d.ts.map +1 -0
- package/dist/shell/index.js +85 -0
- package/dist/shell/index.js.map +1 -0
- package/package.json +17 -0
- package/src/base.ts +24 -0
- package/src/core/channels.ts +47 -0
- package/src/core/identity.ts +125 -0
- package/src/dev/index.ts +119 -0
- package/src/fs/index.ts +272 -0
- package/src/index.ts +31 -0
- package/src/registry.ts +108 -0
- package/src/sandbox.ts +169 -0
- package/src/shell/index.ts +96 -0
- package/tsconfig.json +10 -0
package/src/sandbox.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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];
|