@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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/dist/base.d.ts +17 -0
  3. package/dist/base.d.ts.map +1 -0
  4. package/dist/base.js +12 -0
  5. package/dist/base.js.map +1 -0
  6. package/dist/core/channels.d.ts +9 -0
  7. package/dist/core/channels.d.ts.map +1 -0
  8. package/dist/core/channels.js +39 -0
  9. package/dist/core/channels.js.map +1 -0
  10. package/dist/core/identity.d.ts +5 -0
  11. package/dist/core/identity.d.ts.map +1 -0
  12. package/dist/core/identity.js +108 -0
  13. package/dist/core/identity.js.map +1 -0
  14. package/dist/dev/index.d.ts +7 -0
  15. package/dist/dev/index.d.ts.map +1 -0
  16. package/dist/dev/index.js +113 -0
  17. package/dist/dev/index.js.map +1 -0
  18. package/dist/fs/index.d.ts +11 -0
  19. package/dist/fs/index.d.ts.map +1 -0
  20. package/dist/fs/index.js +240 -0
  21. package/dist/fs/index.js.map +1 -0
  22. package/dist/index.d.ts +14 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +32 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/registry.d.ts +33 -0
  27. package/dist/registry.d.ts.map +1 -0
  28. package/dist/registry.js +96 -0
  29. package/dist/registry.js.map +1 -0
  30. package/dist/sandbox.d.ts +33 -0
  31. package/dist/sandbox.d.ts.map +1 -0
  32. package/dist/sandbox.js +136 -0
  33. package/dist/sandbox.js.map +1 -0
  34. package/dist/shell/index.d.ts +8 -0
  35. package/dist/shell/index.d.ts.map +1 -0
  36. package/dist/shell/index.js +85 -0
  37. package/dist/shell/index.js.map +1 -0
  38. package/package.json +17 -0
  39. package/src/base.ts +24 -0
  40. package/src/core/channels.ts +47 -0
  41. package/src/core/identity.ts +125 -0
  42. package/src/dev/index.ts +119 -0
  43. package/src/fs/index.ts +272 -0
  44. package/src/index.ts +31 -0
  45. package/src/registry.ts +108 -0
  46. package/src/sandbox.ts +169 -0
  47. package/src/shell/index.ts +96 -0
  48. 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];
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": [
8
+ "src"
9
+ ]
10
+ }