musonda 1.0.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.
Files changed (196) hide show
  1. package/IMPLEMENTATION.md +432 -0
  2. package/README.md +119 -0
  3. package/dist/agent/harness.d.ts +25 -0
  4. package/dist/agent/harness.js +215 -0
  5. package/dist/agent/harness.js.map +1 -0
  6. package/dist/agent/openaiResponsesClient.d.ts +35 -0
  7. package/dist/agent/openaiResponsesClient.js +250 -0
  8. package/dist/agent/openaiResponsesClient.js.map +1 -0
  9. package/dist/agent/systemPrompt.d.ts +9 -0
  10. package/dist/agent/systemPrompt.js +100 -0
  11. package/dist/agent/systemPrompt.js.map +1 -0
  12. package/dist/agent/types.d.ts +53 -0
  13. package/dist/agent/types.js +2 -0
  14. package/dist/agent/types.js.map +1 -0
  15. package/dist/cli/commands/chat.d.ts +6 -0
  16. package/dist/cli/commands/chat.js +8 -0
  17. package/dist/cli/commands/chat.js.map +1 -0
  18. package/dist/cli/commands/doctor.d.ts +4 -0
  19. package/dist/cli/commands/doctor.js +336 -0
  20. package/dist/cli/commands/doctor.js.map +1 -0
  21. package/dist/cli/commands/gateway.d.ts +20 -0
  22. package/dist/cli/commands/gateway.js +135 -0
  23. package/dist/cli/commands/gateway.js.map +1 -0
  24. package/dist/cli/commands/help.d.ts +4 -0
  25. package/dist/cli/commands/help.js +159 -0
  26. package/dist/cli/commands/help.js.map +1 -0
  27. package/dist/cli/commands/plugins.d.ts +18 -0
  28. package/dist/cli/commands/plugins.js +250 -0
  29. package/dist/cli/commands/plugins.js.map +1 -0
  30. package/dist/cli/commands/service.d.ts +16 -0
  31. package/dist/cli/commands/service.js +136 -0
  32. package/dist/cli/commands/service.js.map +1 -0
  33. package/dist/cli/commands/setup.d.ts +4 -0
  34. package/dist/cli/commands/setup.js +8 -0
  35. package/dist/cli/commands/setup.js.map +1 -0
  36. package/dist/cli/commands/status.d.ts +4 -0
  37. package/dist/cli/commands/status.js +73 -0
  38. package/dist/cli/commands/status.js.map +1 -0
  39. package/dist/cli/commands/update.d.ts +22 -0
  40. package/dist/cli/commands/update.js +226 -0
  41. package/dist/cli/commands/update.js.map +1 -0
  42. package/dist/cli/daemon.d.ts +33 -0
  43. package/dist/cli/daemon.js +208 -0
  44. package/dist/cli/daemon.js.map +1 -0
  45. package/dist/cli/interactiveMenu.d.ts +5 -0
  46. package/dist/cli/interactiveMenu.js +225 -0
  47. package/dist/cli/interactiveMenu.js.map +1 -0
  48. package/dist/cli/musondaCli.d.ts +30 -0
  49. package/dist/cli/musondaCli.js +269 -0
  50. package/dist/cli/musondaCli.js.map +1 -0
  51. package/dist/cli/ui.d.ts +49 -0
  52. package/dist/cli/ui.js +90 -0
  53. package/dist/cli/ui.js.map +1 -0
  54. package/dist/cli.d.ts +9 -0
  55. package/dist/cli.js +315 -0
  56. package/dist/cli.js.map +1 -0
  57. package/dist/config/loader.d.ts +12 -0
  58. package/dist/config/loader.js +217 -0
  59. package/dist/config/loader.js.map +1 -0
  60. package/dist/config/paths.d.ts +11 -0
  61. package/dist/config/paths.js +18 -0
  62. package/dist/config/paths.js.map +1 -0
  63. package/dist/config/types.d.ts +246 -0
  64. package/dist/config/types.js +155 -0
  65. package/dist/config/types.js.map +1 -0
  66. package/dist/gateway/channels/cli.d.ts +37 -0
  67. package/dist/gateway/channels/cli.js +298 -0
  68. package/dist/gateway/channels/cli.js.map +1 -0
  69. package/dist/gateway/channels/discord.d.ts +18 -0
  70. package/dist/gateway/channels/discord.js +419 -0
  71. package/dist/gateway/channels/discord.js.map +1 -0
  72. package/dist/gateway/channels/slack.d.ts +26 -0
  73. package/dist/gateway/channels/slack.js +470 -0
  74. package/dist/gateway/channels/slack.js.map +1 -0
  75. package/dist/gateway/channels/webhook.d.ts +18 -0
  76. package/dist/gateway/channels/webhook.js +49 -0
  77. package/dist/gateway/channels/webhook.js.map +1 -0
  78. package/dist/gateway/http.d.ts +33 -0
  79. package/dist/gateway/http.js +605 -0
  80. package/dist/gateway/http.js.map +1 -0
  81. package/dist/gateway/server.d.ts +38 -0
  82. package/dist/gateway/server.js +233 -0
  83. package/dist/gateway/server.js.map +1 -0
  84. package/dist/gateway/types.d.ts +28 -0
  85. package/dist/gateway/types.js +2 -0
  86. package/dist/gateway/types.js.map +1 -0
  87. package/dist/index.d.ts +17 -0
  88. package/dist/index.js +32 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/memory/memoryManager.d.ts +22 -0
  91. package/dist/memory/memoryManager.js +200 -0
  92. package/dist/memory/memoryManager.js.map +1 -0
  93. package/dist/memory/storage.d.ts +24 -0
  94. package/dist/memory/storage.js +187 -0
  95. package/dist/memory/storage.js.map +1 -0
  96. package/dist/memory/types.d.ts +43 -0
  97. package/dist/memory/types.js +2 -0
  98. package/dist/memory/types.js.map +1 -0
  99. package/dist/plugins/manifest.d.ts +23 -0
  100. package/dist/plugins/manifest.js +185 -0
  101. package/dist/plugins/manifest.js.map +1 -0
  102. package/dist/plugins/mcp/mcpBridge.d.ts +31 -0
  103. package/dist/plugins/mcp/mcpBridge.js +115 -0
  104. package/dist/plugins/mcp/mcpBridge.js.map +1 -0
  105. package/dist/plugins/mcp/mcpClient.d.ts +43 -0
  106. package/dist/plugins/mcp/mcpClient.js +309 -0
  107. package/dist/plugins/mcp/mcpClient.js.map +1 -0
  108. package/dist/plugins/mcp/mcpConfig.d.ts +34 -0
  109. package/dist/plugins/mcp/mcpConfig.js +326 -0
  110. package/dist/plugins/mcp/mcpConfig.js.map +1 -0
  111. package/dist/plugins/mcp/types.d.ts +48 -0
  112. package/dist/plugins/mcp/types.js +2 -0
  113. package/dist/plugins/mcp/types.js.map +1 -0
  114. package/dist/plugins/pluginManager.d.ts +164 -0
  115. package/dist/plugins/pluginManager.js +1016 -0
  116. package/dist/plugins/pluginManager.js.map +1 -0
  117. package/dist/plugins/skills/frontmatterParser.d.ts +28 -0
  118. package/dist/plugins/skills/frontmatterParser.js +189 -0
  119. package/dist/plugins/skills/frontmatterParser.js.map +1 -0
  120. package/dist/plugins/skills/skillDiscovery.d.ts +15 -0
  121. package/dist/plugins/skills/skillDiscovery.js +148 -0
  122. package/dist/plugins/skills/skillDiscovery.js.map +1 -0
  123. package/dist/plugins/skills/skillTools.d.ts +8 -0
  124. package/dist/plugins/skills/skillTools.js +195 -0
  125. package/dist/plugins/skills/skillTools.js.map +1 -0
  126. package/dist/plugins/skills/types.d.ts +25 -0
  127. package/dist/plugins/skills/types.js +2 -0
  128. package/dist/plugins/skills/types.js.map +1 -0
  129. package/dist/plugins/types.d.ts +30 -0
  130. package/dist/plugins/types.js +2 -0
  131. package/dist/plugins/types.js.map +1 -0
  132. package/dist/scheduler/scheduler.d.ts +25 -0
  133. package/dist/scheduler/scheduler.js +239 -0
  134. package/dist/scheduler/scheduler.js.map +1 -0
  135. package/dist/scheduler/types.d.ts +39 -0
  136. package/dist/scheduler/types.js +2 -0
  137. package/dist/scheduler/types.js.map +1 -0
  138. package/dist/security/childProcessEnv.d.ts +5 -0
  139. package/dist/security/childProcessEnv.js +42 -0
  140. package/dist/security/childProcessEnv.js.map +1 -0
  141. package/dist/security/fileSandbox.d.ts +153 -0
  142. package/dist/security/fileSandbox.js +779 -0
  143. package/dist/security/fileSandbox.js.map +1 -0
  144. package/dist/service/index.d.ts +30 -0
  145. package/dist/service/index.js +41 -0
  146. package/dist/service/index.js.map +1 -0
  147. package/dist/service/systemd.d.ts +36 -0
  148. package/dist/service/systemd.js +409 -0
  149. package/dist/service/systemd.js.map +1 -0
  150. package/dist/service/types.d.ts +118 -0
  151. package/dist/service/types.js +6 -0
  152. package/dist/service/types.js.map +1 -0
  153. package/dist/tools/builtin/calcTools.d.ts +2 -0
  154. package/dist/tools/builtin/calcTools.js +51 -0
  155. package/dist/tools/builtin/calcTools.js.map +1 -0
  156. package/dist/tools/builtin/fileTools.d.ts +11 -0
  157. package/dist/tools/builtin/fileTools.js +352 -0
  158. package/dist/tools/builtin/fileTools.js.map +1 -0
  159. package/dist/tools/builtin/memoryTools.d.ts +4 -0
  160. package/dist/tools/builtin/memoryTools.js +123 -0
  161. package/dist/tools/builtin/memoryTools.js.map +1 -0
  162. package/dist/tools/builtin/noteTools.d.ts +4 -0
  163. package/dist/tools/builtin/noteTools.js +91 -0
  164. package/dist/tools/builtin/noteTools.js.map +1 -0
  165. package/dist/tools/builtin/pluginTools.d.ts +29 -0
  166. package/dist/tools/builtin/pluginTools.js +406 -0
  167. package/dist/tools/builtin/pluginTools.js.map +1 -0
  168. package/dist/tools/builtin/scheduleTools.d.ts +4 -0
  169. package/dist/tools/builtin/scheduleTools.js +156 -0
  170. package/dist/tools/builtin/scheduleTools.js.map +1 -0
  171. package/dist/tools/builtin/timeTools.d.ts +2 -0
  172. package/dist/tools/builtin/timeTools.js +41 -0
  173. package/dist/tools/builtin/timeTools.js.map +1 -0
  174. package/dist/tools/builtin/webTools.d.ts +12 -0
  175. package/dist/tools/builtin/webTools.js +325 -0
  176. package/dist/tools/builtin/webTools.js.map +1 -0
  177. package/dist/tools/registry.d.ts +15 -0
  178. package/dist/tools/registry.js +111 -0
  179. package/dist/tools/registry.js.map +1 -0
  180. package/dist/tools/types.d.ts +41 -0
  181. package/dist/tools/types.js +2 -0
  182. package/dist/tools/types.js.map +1 -0
  183. package/dist/utils/logger.d.ts +29 -0
  184. package/dist/utils/logger.js +124 -0
  185. package/dist/utils/logger.js.map +1 -0
  186. package/dist/wizard/onboarding.d.ts +3 -0
  187. package/dist/wizard/onboarding.js +408 -0
  188. package/dist/wizard/onboarding.js.map +1 -0
  189. package/package.json +69 -0
  190. package/plugins/example-calc/mcp.json +11 -0
  191. package/plugins/example-calc/plugin.json +12 -0
  192. package/plugins/example-calc/server.mjs +92 -0
  193. package/plugins/example-calc/skills/math-assistant/SKILL.md +15 -0
  194. package/plugins/plugin-builder/plugin.json +12 -0
  195. package/plugins/plugin-builder/skills/create-plugin/SKILL.md +149 -0
  196. package/plugins/plugin-builder/skills/manage-plugins/SKILL.md +157 -0
@@ -0,0 +1,779 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getMusondaHomeDir } from '../config/paths.js';
4
+ import { logger } from '../utils/logger.js';
5
+ export class SandboxSecurityError extends Error {
6
+ code;
7
+ path;
8
+ details;
9
+ constructor(message, code = 'SANDBOX_SECURITY_VIOLATION', extra) {
10
+ super(message);
11
+ this.name = 'SandboxSecurityError';
12
+ this.code = code;
13
+ this.path = extra?.path;
14
+ this.details = extra?.details;
15
+ }
16
+ }
17
+ /**
18
+ * Match a string against a glob pattern.
19
+ * Supports `**` (recursive directories), `*` (any chars within path segment), and `?`.
20
+ */
21
+ export function matchGlob(str, pattern) {
22
+ const normStr = str.replace(/\\/g, '/').replace(/^\.\//, '');
23
+ const normPattern = pattern.replace(/\\/g, '/').replace(/^\.\//, '');
24
+ let regexStr = '^';
25
+ let i = 0;
26
+ while (i < normPattern.length) {
27
+ const c = normPattern[i];
28
+ if (c === '*' && normPattern[i + 1] === '*') {
29
+ if (normPattern[i + 2] === '/') {
30
+ regexStr += '(?:.*\\/)?';
31
+ i += 3;
32
+ }
33
+ else {
34
+ regexStr += '.*';
35
+ i += 2;
36
+ }
37
+ }
38
+ else if (c === '*') {
39
+ regexStr += '[^/]*';
40
+ i++;
41
+ }
42
+ else if (c === '?') {
43
+ regexStr += '[^/]';
44
+ i++;
45
+ }
46
+ else if (['.', '+', '^', '$', '(', ')', '[', ']', '{', '}', '|'].includes(c)) {
47
+ regexStr += '\\' + c;
48
+ i++;
49
+ }
50
+ else {
51
+ regexStr += c;
52
+ i++;
53
+ }
54
+ }
55
+ regexStr += '$';
56
+ return new RegExp(regexStr, 'i').test(normStr);
57
+ }
58
+ /**
59
+ * Normalize a path for platform-accurate comparison.
60
+ * On Windows, paths are case-insensitive.
61
+ */
62
+ function normalizeForComparison(p) {
63
+ const norm = path.normalize(p);
64
+ return process.platform === 'win32' ? norm.toLowerCase() : norm;
65
+ }
66
+ /**
67
+ * Check if child is equal to parent or inside parent directory.
68
+ */
69
+ export function isSubPath(child, parent) {
70
+ const childNorm = normalizeForComparison(child);
71
+ const parentNorm = normalizeForComparison(parent);
72
+ if (childNorm === parentNorm)
73
+ return true;
74
+ const rel = path.relative(parentNorm, childNorm);
75
+ return !rel.startsWith('..') && !path.isAbsolute(rel);
76
+ }
77
+ /**
78
+ * Canonical realpath with Windows long-path prefix stripping.
79
+ */
80
+ export function getRealPath(p) {
81
+ let real = fs.realpathSync(p);
82
+ if (real.startsWith('\\\\?\\')) {
83
+ real = real.substring(4);
84
+ }
85
+ return path.normalize(real);
86
+ }
87
+ export class FileSandbox {
88
+ config;
89
+ normalizedRoots = [];
90
+ defaultRootPath = '';
91
+ constructor(config) {
92
+ this.config = config;
93
+ this.initRoots();
94
+ }
95
+ initRoots() {
96
+ const roots = this.config.allowedRoots.length > 0 ? this.config.allowedRoots : ['./data/workspace'];
97
+ for (const item of roots) {
98
+ let rule;
99
+ if (typeof item === 'string') {
100
+ rule = { path: item, readOnly: false };
101
+ }
102
+ else {
103
+ rule = item;
104
+ }
105
+ const absPath = path.resolve(process.cwd(), rule.path);
106
+ if (!fs.existsSync(absPath)) {
107
+ try {
108
+ fs.mkdirSync(absPath, { recursive: true });
109
+ }
110
+ catch (err) {
111
+ logger.warn(`Could not create sandbox root directory ${absPath}: ${err.message}`);
112
+ }
113
+ }
114
+ let canonical = absPath;
115
+ if (fs.existsSync(absPath)) {
116
+ try {
117
+ canonical = getRealPath(absPath);
118
+ }
119
+ catch {
120
+ canonical = path.normalize(absPath);
121
+ }
122
+ }
123
+ this.normalizedRoots.push({
124
+ rawPath: rule.path,
125
+ resolvedPath: canonical,
126
+ readOnly: Boolean(rule.readOnly),
127
+ description: rule.description,
128
+ });
129
+ }
130
+ if (this.config.defaultRoot) {
131
+ const defaultAbs = path.resolve(process.cwd(), this.config.defaultRoot);
132
+ this.defaultRootPath = fs.existsSync(defaultAbs) ? getRealPath(defaultAbs) : path.normalize(defaultAbs);
133
+ }
134
+ else if (this.normalizedRoots.length > 0) {
135
+ this.defaultRootPath = this.normalizedRoots[0].resolvedPath;
136
+ }
137
+ else {
138
+ const fallback = path.resolve(process.cwd(), 'data/workspace');
139
+ if (!fs.existsSync(fallback))
140
+ fs.mkdirSync(fallback, { recursive: true });
141
+ this.defaultRootPath = getRealPath(fallback);
142
+ }
143
+ }
144
+ getAllowedRoots() {
145
+ return [...this.normalizedRoots];
146
+ }
147
+ getDefaultRootPath() {
148
+ return this.defaultRootPath;
149
+ }
150
+ /**
151
+ * Validate path syntax to block malicious tricks:
152
+ * - Null bytes
153
+ * - ASCII control characters
154
+ * - UNC paths
155
+ * - Windows Alternate Data Streams (colon in segments)
156
+ * - Windows reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
157
+ */
158
+ validatePathSyntax(rawPath) {
159
+ if (typeof rawPath !== 'string' || !rawPath.trim()) {
160
+ throw new SandboxSecurityError('File path must be a non-empty string.', 'EMPTY_PATH');
161
+ }
162
+ // Check for null bytes
163
+ if (rawPath.includes('\0')) {
164
+ throw new SandboxSecurityError('Path contains invalid null byte.', 'NULL_BYTE_DETECTED', { path: rawPath });
165
+ }
166
+ // Check for ASCII control characters (0x01 - 0x1F)
167
+ if (/[\x01-\x1F]/.test(rawPath)) {
168
+ throw new SandboxSecurityError('Path contains illegal control characters.', 'CONTROL_CHAR_DETECTED', { path: rawPath });
169
+ }
170
+ // Reject UNC network paths
171
+ if (rawPath.startsWith('\\\\') || rawPath.startsWith('//')) {
172
+ throw new SandboxSecurityError('UNC network paths are not permitted in sandbox.', 'UNC_PATH_BLOCKED', { path: rawPath });
173
+ }
174
+ // Inspect individual path segments
175
+ const segments = rawPath.split(/[/\\]+/).filter(Boolean);
176
+ for (let idx = 0; idx < segments.length; idx++) {
177
+ const segment = segments[idx];
178
+ // Check Windows reserved device names
179
+ if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i.test(segment)) {
180
+ throw new SandboxSecurityError(`Path contains reserved system device name "${segment}".`, 'RESERVED_DEVICE_NAME', { path: rawPath });
181
+ }
182
+ // Check for Alternate Data Streams or unexpected colons
183
+ // Index 0 in Windows absolute paths like "C:" is permitted
184
+ if (segment.includes(':')) {
185
+ const isWindowsDrive = idx === 0 && /^[a-zA-Z]:$/.test(segment);
186
+ if (!isWindowsDrive) {
187
+ throw new SandboxSecurityError(`Path contains illegal colon in component "${segment}" (Alternate Data Streams prohibited).`, 'ADS_STREAM_BLOCKED', { path: rawPath });
188
+ }
189
+ }
190
+ }
191
+ }
192
+ /**
193
+ * Validates that a path is fully contained within the allowed sandbox roots,
194
+ * does not escape via traversal or symlinks, does not match blocked patterns,
195
+ * and respects permission controls (e.g. readOnly).
196
+ */
197
+ validatePath(rawPath, operation) {
198
+ if (!this.config.enabled) {
199
+ // If sandboxing is explicitly disabled by config, still prevent null bytes and resolve path
200
+ this.validatePathSyntax(rawPath);
201
+ const res = path.isAbsolute(rawPath) ? path.resolve(rawPath) : path.resolve(this.defaultRootPath, rawPath);
202
+ return {
203
+ resolvedPath: res,
204
+ rootRule: {
205
+ rawPath: '.',
206
+ resolvedPath: process.cwd(),
207
+ readOnly: false,
208
+ },
209
+ };
210
+ }
211
+ // 1. Validate syntax guardrails
212
+ this.validatePathSyntax(rawPath);
213
+ // 2. Resolve to absolute path
214
+ const resolved = path.isAbsolute(rawPath)
215
+ ? path.resolve(rawPath)
216
+ : path.resolve(this.defaultRootPath, rawPath);
217
+ // Guard against accessing system user security directory (~/.musonda)
218
+ const musondaHome = getMusondaHomeDir();
219
+ const isTargetingMusonda = isSubPath(resolved, musondaHome) ||
220
+ (fs.existsSync(musondaHome) && isSubPath(resolved, getRealPath(musondaHome)));
221
+ if (isTargetingMusonda) {
222
+ logger.warn(`[SANDBOX_BLOCKED] Attempted access to user security directory "${resolved}".`);
223
+ throw new SandboxSecurityError(`Access denied: System user security directory "${musondaHome}" is protected by security guardrails.`, 'SECURITY_ROOT_PROTECTED', { path: rawPath });
224
+ }
225
+ // 3. Find matching allowed root
226
+ let matchedRoot;
227
+ for (const root of this.normalizedRoots) {
228
+ if (isSubPath(resolved, root.resolvedPath)) {
229
+ matchedRoot = root;
230
+ break;
231
+ }
232
+ }
233
+ if (!matchedRoot) {
234
+ logger.warn(`[SANDBOX_BLOCKED] Path "${rawPath}" resolves to "${resolved}" which is outside allowed sandbox roots.`);
235
+ throw new SandboxSecurityError(`Access denied: Path "${rawPath}" resolves outside of permitted sandbox directories.`, 'PATH_OUTSIDE_SANDBOX', {
236
+ path: rawPath,
237
+ details: {
238
+ resolved,
239
+ allowedRoots: this.normalizedRoots.map((r) => r.rawPath),
240
+ },
241
+ });
242
+ }
243
+ // 4. Symlink resolution protection
244
+ if (fs.existsSync(resolved)) {
245
+ let realTarget;
246
+ try {
247
+ realTarget = getRealPath(resolved);
248
+ }
249
+ catch (err) {
250
+ throw new SandboxSecurityError(`Unable to resolve canonical real path: ${err.message}`, 'REALPATH_RESOLUTION_FAILED', { path: rawPath });
251
+ }
252
+ // Verify that canonical real target is inside an allowed root
253
+ const insideAnyAllowed = this.normalizedRoots.some((r) => isSubPath(realTarget, r.resolvedPath));
254
+ if (!insideAnyAllowed) {
255
+ logger.warn(`[SANDBOX_BLOCKED] Symlink escape: "${resolved}" targets "${realTarget}".`);
256
+ throw new SandboxSecurityError(`Access denied: Target resolves via symbolic link to "${realTarget}", which is outside allowed sandbox roots.`, 'SYMLINK_ESCAPE_DETECTED', { path: rawPath, details: { realTarget } });
257
+ }
258
+ }
259
+ else {
260
+ // Path does not exist yet (e.g. creating a new file).
261
+ // Find deepest existing ancestor and ensure its real path does not escape
262
+ let current = path.dirname(resolved);
263
+ while (!fs.existsSync(current)) {
264
+ const parent = path.dirname(current);
265
+ if (parent === current)
266
+ break;
267
+ current = parent;
268
+ }
269
+ if (fs.existsSync(current)) {
270
+ try {
271
+ const realAncestor = getRealPath(current);
272
+ const ancestorInside = this.normalizedRoots.some((r) => isSubPath(realAncestor, r.resolvedPath));
273
+ if (!ancestorInside) {
274
+ logger.warn(`[SANDBOX_BLOCKED] Parent directory resolves to "${realAncestor}" outside sandbox.`);
275
+ throw new SandboxSecurityError(`Access denied: Parent directory resolves via symbolic link outside allowed sandbox roots.`, 'SYMLINK_ESCAPE_DETECTED', { path: rawPath });
276
+ }
277
+ }
278
+ catch (err) {
279
+ throw new SandboxSecurityError(`Unable to verify ancestor path: ${err.message}`, 'REALPATH_RESOLUTION_FAILED', { path: rawPath });
280
+ }
281
+ }
282
+ }
283
+ // 5. Blocked patterns protection
284
+ const basename = path.basename(resolved);
285
+ const relativeFromRoot = path.relative(matchedRoot.resolvedPath, resolved).replace(/\\/g, '/');
286
+ const relativeFromRaw = rawPath.replace(/\\/g, '/');
287
+ for (const pattern of this.config.blockedPatterns) {
288
+ if (matchGlob(basename, pattern) ||
289
+ matchGlob(relativeFromRoot, pattern) ||
290
+ matchGlob(relativeFromRaw, pattern)) {
291
+ logger.warn(`[SANDBOX_BLOCKED] Path "${rawPath}" matches restricted pattern "${pattern}".`);
292
+ throw new SandboxSecurityError(`Access denied: Path "${rawPath}" matches protected file pattern "${pattern}".`, 'RESTRICTED_PATTERN', { path: rawPath, details: { pattern } });
293
+ }
294
+ }
295
+ // 6. Permission & Operation guardrails
296
+ if (operation === 'write' || operation === 'delete') {
297
+ if (matchedRoot.readOnly) {
298
+ throw new SandboxSecurityError(`Access denied: Directory "${matchedRoot.rawPath}" is mounted as read-only. Operation "${operation}" is forbidden.`, 'READ_ONLY_VIOLATION', { path: rawPath, details: { root: matchedRoot.rawPath } });
299
+ }
300
+ }
301
+ if (operation === 'delete') {
302
+ if (!this.config.allowDelete) {
303
+ throw new SandboxSecurityError('Access denied: File deletion operations are disabled by policy.', 'DELETE_DISABLED', { path: rawPath });
304
+ }
305
+ // Guard against deleting the root sandbox directory itself
306
+ if (normalizeForComparison(resolved) === normalizeForComparison(matchedRoot.resolvedPath)) {
307
+ throw new SandboxSecurityError(`Access denied: Cannot delete root sandbox directory "${rawPath}".`, 'ROOT_DELETION_PROHIBITED', { path: rawPath });
308
+ }
309
+ }
310
+ return { resolvedPath: resolved, rootRule: matchedRoot };
311
+ }
312
+ // =========================================================================
313
+ // File Operations (Guarded)
314
+ // =========================================================================
315
+ readFile(rawPath, options) {
316
+ const { resolvedPath } = this.validatePath(rawPath, 'read');
317
+ if (!fs.existsSync(resolvedPath)) {
318
+ throw new SandboxSecurityError(`File not found: "${rawPath}".`, 'FILE_NOT_FOUND', { path: rawPath });
319
+ }
320
+ const stat = fs.statSync(resolvedPath);
321
+ if (stat.isDirectory()) {
322
+ throw new SandboxSecurityError(`Path "${rawPath}" is a directory, not a file. Use file_list to inspect directories.`, 'IS_DIRECTORY', { path: rawPath });
323
+ }
324
+ const effectiveMaxBytes = options?.maxBytes
325
+ ? Math.min(options.maxBytes, this.config.maxReadSizeBytes)
326
+ : this.config.maxReadSizeBytes;
327
+ if (stat.size > effectiveMaxBytes && !options?.startLine && !options?.endLine) {
328
+ throw new SandboxSecurityError(`File size (${stat.size} bytes) exceeds maximum read limit (${effectiveMaxBytes} bytes). Provide startLine and endLine to paginate through this file.`, 'FILE_TOO_LARGE', { path: rawPath, details: { size: stat.size, maxAllowed: effectiveMaxBytes } });
329
+ }
330
+ const encoding = options?.encoding || 'utf-8';
331
+ if (encoding === 'base64') {
332
+ const buffer = fs.readFileSync(resolvedPath);
333
+ return {
334
+ path: rawPath,
335
+ content: buffer.toString('base64'),
336
+ totalBytes: stat.size,
337
+ };
338
+ }
339
+ const fullContent = fs.readFileSync(resolvedPath, 'utf-8');
340
+ if (options?.startLine !== undefined || options?.endLine !== undefined) {
341
+ const lines = fullContent.split(/\r?\n/);
342
+ const totalLines = lines.length;
343
+ const start = Math.max(1, options.startLine || 1);
344
+ const end = options.endLine ? Math.min(totalLines, options.endLine) : totalLines;
345
+ if (start > totalLines) {
346
+ return {
347
+ path: rawPath,
348
+ content: '',
349
+ startLine: start,
350
+ endLine: end,
351
+ totalLines,
352
+ totalBytes: stat.size,
353
+ };
354
+ }
355
+ const slicedLines = lines.slice(start - 1, end);
356
+ return {
357
+ path: rawPath,
358
+ content: slicedLines.join('\n'),
359
+ startLine: start,
360
+ endLine: end,
361
+ totalLines,
362
+ totalBytes: stat.size,
363
+ truncated: start > 1 || end < totalLines,
364
+ };
365
+ }
366
+ return {
367
+ path: rawPath,
368
+ content: fullContent,
369
+ totalBytes: stat.size,
370
+ };
371
+ }
372
+ writeFile(rawPath, content, options) {
373
+ const { resolvedPath } = this.validatePath(rawPath, 'write');
374
+ const encoding = options?.encoding || 'utf-8';
375
+ const buffer = Buffer.from(content, encoding);
376
+ if (buffer.length > this.config.maxWriteSizeBytes) {
377
+ throw new SandboxSecurityError(`Content size (${buffer.length} bytes) exceeds maximum write limit (${this.config.maxWriteSizeBytes} bytes).`, 'CONTENT_TOO_LARGE', { path: rawPath, details: { size: buffer.length, maxAllowed: this.config.maxWriteSizeBytes } });
378
+ }
379
+ if (fs.existsSync(resolvedPath)) {
380
+ if (options?.overwrite === false) {
381
+ throw new SandboxSecurityError(`File "${rawPath}" already exists and overwrite is set to false.`, 'FILE_EXISTS', { path: rawPath });
382
+ }
383
+ const stat = fs.statSync(resolvedPath);
384
+ if (stat.isDirectory()) {
385
+ throw new SandboxSecurityError(`Cannot write to "${rawPath}" because it is an existing directory.`, 'IS_DIRECTORY', { path: rawPath });
386
+ }
387
+ }
388
+ const parentDir = path.dirname(resolvedPath);
389
+ if (!fs.existsSync(parentDir)) {
390
+ fs.mkdirSync(parentDir, { recursive: true });
391
+ }
392
+ fs.writeFileSync(resolvedPath, buffer);
393
+ logger.info(`[FILE_SANDBOX] Wrote ${buffer.length} bytes to "${rawPath}"`);
394
+ return {
395
+ success: true,
396
+ path: rawPath,
397
+ bytesWritten: buffer.length,
398
+ };
399
+ }
400
+ editFile(rawPath, target, replacement, allowMultiple = false) {
401
+ const { resolvedPath } = this.validatePath(rawPath, 'write');
402
+ if (!fs.existsSync(resolvedPath)) {
403
+ throw new SandboxSecurityError(`File not found for edit: "${rawPath}".`, 'FILE_NOT_FOUND', { path: rawPath });
404
+ }
405
+ const stat = fs.statSync(resolvedPath);
406
+ if (stat.isDirectory()) {
407
+ throw new SandboxSecurityError(`Cannot edit "${rawPath}" because it is a directory.`, 'IS_DIRECTORY', { path: rawPath });
408
+ }
409
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
410
+ if (!target) {
411
+ throw new SandboxSecurityError('Target string to replace cannot be empty.', 'EMPTY_TARGET');
412
+ }
413
+ // Count occurrences
414
+ let count = 0;
415
+ let pos = 0;
416
+ while ((pos = content.indexOf(target, pos)) !== -1) {
417
+ count++;
418
+ pos += target.length;
419
+ }
420
+ if (count === 0) {
421
+ throw new SandboxSecurityError(`Target text snippet was not found in file "${rawPath}". Please check the text and try again.`, 'TARGET_NOT_FOUND', { path: rawPath });
422
+ }
423
+ if (count > 1 && !allowMultiple) {
424
+ throw new SandboxSecurityError(`Target snippet was found ${count} times in "${rawPath}". Provide more surrounding lines to uniquely identify the section, or set allowMultiple: true.`, 'AMBIGUOUS_TARGET', { path: rawPath, details: { occurrences: count } });
425
+ }
426
+ let newContent;
427
+ if (allowMultiple) {
428
+ newContent = content.split(target).join(replacement);
429
+ }
430
+ else {
431
+ newContent = content.replace(target, replacement);
432
+ }
433
+ const buffer = Buffer.from(newContent, 'utf-8');
434
+ if (buffer.length > this.config.maxWriteSizeBytes) {
435
+ throw new SandboxSecurityError(`Edited file size (${buffer.length} bytes) exceeds maximum write limit (${this.config.maxWriteSizeBytes} bytes).`, 'CONTENT_TOO_LARGE', { path: rawPath });
436
+ }
437
+ fs.writeFileSync(resolvedPath, buffer);
438
+ logger.info(`[FILE_SANDBOX] Edited "${rawPath}": replaced ${count} occurrence(s)`);
439
+ return {
440
+ success: true,
441
+ path: rawPath,
442
+ occurrencesReplaced: count,
443
+ bytesWritten: buffer.length,
444
+ };
445
+ }
446
+ appendFile(rawPath, content) {
447
+ const { resolvedPath } = this.validatePath(rawPath, 'write');
448
+ const buffer = Buffer.from(content, 'utf-8');
449
+ const existingSize = fs.existsSync(resolvedPath) ? fs.statSync(resolvedPath).size : 0;
450
+ if (existingSize + buffer.length > this.config.maxWriteSizeBytes) {
451
+ throw new SandboxSecurityError(`Resulting file size (${existingSize + buffer.length} bytes) exceeds maximum write limit (${this.config.maxWriteSizeBytes} bytes).`, 'CONTENT_TOO_LARGE', { path: rawPath });
452
+ }
453
+ const parentDir = path.dirname(resolvedPath);
454
+ if (!fs.existsSync(parentDir)) {
455
+ fs.mkdirSync(parentDir, { recursive: true });
456
+ }
457
+ fs.appendFileSync(resolvedPath, buffer);
458
+ logger.info(`[FILE_SANDBOX] Appended ${buffer.length} bytes to "${rawPath}"`);
459
+ return {
460
+ success: true,
461
+ path: rawPath,
462
+ bytesAppended: buffer.length,
463
+ };
464
+ }
465
+ listFiles(rawPath = '.', options) {
466
+ const { resolvedPath, rootRule } = this.validatePath(rawPath, 'read');
467
+ if (!fs.existsSync(resolvedPath)) {
468
+ throw new SandboxSecurityError(`Directory not found: "${rawPath}".`, 'DIRECTORY_NOT_FOUND', { path: rawPath });
469
+ }
470
+ const stat = fs.statSync(resolvedPath);
471
+ if (!stat.isDirectory()) {
472
+ throw new SandboxSecurityError(`Path "${rawPath}" is a file, not a directory.`, 'NOT_A_DIRECTORY', { path: rawPath });
473
+ }
474
+ const entries = [];
475
+ const maxEntries = this.config.maxListEntries;
476
+ let truncated = false;
477
+ const recursive = Boolean(options?.recursive);
478
+ const maxDepth = options?.maxDepth !== undefined ? options.maxDepth : 2;
479
+ const traverse = (currentDir, depth) => {
480
+ if (entries.length >= maxEntries) {
481
+ truncated = true;
482
+ return;
483
+ }
484
+ let dirents;
485
+ try {
486
+ dirents = fs.readdirSync(currentDir, { withFileTypes: true });
487
+ }
488
+ catch (err) {
489
+ logger.warn(`Could not read directory ${currentDir}: ${err.message}`);
490
+ return;
491
+ }
492
+ for (const dirent of dirents) {
493
+ if (entries.length >= maxEntries) {
494
+ truncated = true;
495
+ return;
496
+ }
497
+ const fullPath = path.join(currentDir, dirent.name);
498
+ const relFromTarget = path.relative(resolvedPath, fullPath).replace(/\\/g, '/');
499
+ const relFromRoot = path.relative(rootRule.resolvedPath, fullPath).replace(/\\/g, '/');
500
+ // Check if item matches blocked patterns
501
+ const isBlocked = this.config.blockedPatterns.some((p) => matchGlob(dirent.name, p) || matchGlob(relFromRoot, p));
502
+ if (isBlocked) {
503
+ continue;
504
+ }
505
+ let isDir = dirent.isDirectory();
506
+ let isFile = dirent.isFile();
507
+ let size = 0;
508
+ let modifiedAt = '';
509
+ try {
510
+ const s = fs.statSync(fullPath);
511
+ size = s.size;
512
+ modifiedAt = s.mtime.toISOString();
513
+ isDir = s.isDirectory();
514
+ isFile = s.isFile();
515
+ }
516
+ catch {
517
+ // In case of broken link or permission
518
+ }
519
+ entries.push({
520
+ name: dirent.name,
521
+ relativePath: relFromTarget,
522
+ isDirectory: isDir,
523
+ isFile: isFile,
524
+ sizeBytes: size,
525
+ modifiedAt,
526
+ });
527
+ if (isDir && recursive && depth < maxDepth) {
528
+ traverse(fullPath, depth + 1);
529
+ }
530
+ }
531
+ };
532
+ traverse(resolvedPath, 1);
533
+ return {
534
+ directory: rawPath,
535
+ totalEntries: entries.length,
536
+ truncated,
537
+ entries,
538
+ };
539
+ }
540
+ deletePath(rawPath, recursive = false) {
541
+ const { resolvedPath, rootRule } = this.validatePath(rawPath, 'delete');
542
+ if (!fs.existsSync(resolvedPath)) {
543
+ throw new SandboxSecurityError(`Path not found for deletion: "${rawPath}".`, 'FILE_NOT_FOUND', { path: rawPath });
544
+ }
545
+ const stat = fs.statSync(resolvedPath);
546
+ if (stat.isDirectory()) {
547
+ const children = fs.readdirSync(resolvedPath);
548
+ if (children.length > 0 && !recursive) {
549
+ throw new SandboxSecurityError(`Directory "${rawPath}" is not empty. Set recursive: true to delete non-empty directories.`, 'DIRECTORY_NOT_EMPTY', { path: rawPath, details: { itemCount: children.length } });
550
+ }
551
+ // Verify no blocked files exist inside before recursive deletion
552
+ if (recursive) {
553
+ this.verifyNoBlockedFilesInTree(resolvedPath, rootRule.resolvedPath);
554
+ }
555
+ fs.rmSync(resolvedPath, { recursive: true, force: true });
556
+ }
557
+ else {
558
+ fs.unlinkSync(resolvedPath);
559
+ }
560
+ logger.info(`[FILE_SANDBOX] Deleted "${rawPath}" (recursive: ${recursive})`);
561
+ return {
562
+ success: true,
563
+ path: rawPath,
564
+ deleted: true,
565
+ };
566
+ }
567
+ verifyNoBlockedFilesInTree(dir, rootCanonical) {
568
+ const dirents = fs.readdirSync(dir, { withFileTypes: true });
569
+ for (const d of dirents) {
570
+ const full = path.join(dir, d.name);
571
+ const rel = path.relative(rootCanonical, full).replace(/\\/g, '/');
572
+ for (const pattern of this.config.blockedPatterns) {
573
+ if (matchGlob(d.name, pattern) || matchGlob(rel, pattern)) {
574
+ throw new SandboxSecurityError(`Cannot recursively delete directory containing protected file "${d.name}" (pattern "${pattern}").`, 'PROTECTED_FILE_IN_TREE', { path: full, details: { pattern } });
575
+ }
576
+ }
577
+ if (d.isDirectory()) {
578
+ this.verifyNoBlockedFilesInTree(full, rootCanonical);
579
+ }
580
+ }
581
+ }
582
+ getFileInfo(rawPath) {
583
+ const { resolvedPath, rootRule } = this.validatePath(rawPath, 'read');
584
+ if (!fs.existsSync(resolvedPath)) {
585
+ return {
586
+ exists: false,
587
+ path: rawPath,
588
+ };
589
+ }
590
+ const stat = fs.statSync(resolvedPath);
591
+ return {
592
+ exists: true,
593
+ path: rawPath,
594
+ isFile: stat.isFile(),
595
+ isDirectory: stat.isDirectory(),
596
+ isSymbolicLink: stat.isSymbolicLink(),
597
+ sizeBytes: stat.size,
598
+ createdAt: stat.birthtime.toISOString(),
599
+ modifiedAt: stat.mtime.toISOString(),
600
+ readOnly: rootRule.readOnly,
601
+ };
602
+ }
603
+ createDirectory(rawPath) {
604
+ const { resolvedPath } = this.validatePath(rawPath, 'write');
605
+ if (fs.existsSync(resolvedPath)) {
606
+ const stat = fs.statSync(resolvedPath);
607
+ if (stat.isDirectory()) {
608
+ return { success: true, path: rawPath, created: false };
609
+ }
610
+ throw new SandboxSecurityError(`Path "${rawPath}" already exists and is a file.`, 'PATH_EXISTS_AS_FILE', { path: rawPath });
611
+ }
612
+ fs.mkdirSync(resolvedPath, { recursive: true });
613
+ logger.info(`[FILE_SANDBOX] Created directory "${rawPath}"`);
614
+ return {
615
+ success: true,
616
+ path: rawPath,
617
+ created: true,
618
+ };
619
+ }
620
+ searchFiles(pattern, rawPath = '.', maxResults = 50) {
621
+ const { resolvedPath, rootRule } = this.validatePath(rawPath, 'read');
622
+ if (!fs.existsSync(resolvedPath)) {
623
+ throw new SandboxSecurityError(`Search path not found: "${rawPath}".`, 'DIRECTORY_NOT_FOUND', { path: rawPath });
624
+ }
625
+ const matches = [];
626
+ const limit = Math.min(maxResults, this.config.maxListEntries);
627
+ let truncated = false;
628
+ const traverse = (currentDir, depth) => {
629
+ if (matches.length >= limit || depth > 10) {
630
+ if (matches.length >= limit)
631
+ truncated = true;
632
+ return;
633
+ }
634
+ let dirents;
635
+ try {
636
+ dirents = fs.readdirSync(currentDir, { withFileTypes: true });
637
+ }
638
+ catch {
639
+ return;
640
+ }
641
+ for (const d of dirents) {
642
+ if (matches.length >= limit) {
643
+ truncated = true;
644
+ return;
645
+ }
646
+ const fullPath = path.join(currentDir, d.name);
647
+ const relFromRoot = path.relative(rootRule.resolvedPath, fullPath).replace(/\\/g, '/');
648
+ const relFromSearch = path.relative(resolvedPath, fullPath).replace(/\\/g, '/');
649
+ // Check blocked patterns
650
+ const isBlocked = this.config.blockedPatterns.some((p) => matchGlob(d.name, p) || matchGlob(relFromRoot, p));
651
+ if (isBlocked)
652
+ continue;
653
+ // Check match
654
+ if (matchGlob(d.name, pattern) || d.name.toLowerCase().includes(pattern.toLowerCase())) {
655
+ let size = 0;
656
+ try {
657
+ size = fs.statSync(fullPath).size;
658
+ }
659
+ catch { }
660
+ matches.push({
661
+ name: d.name,
662
+ relativePath: relFromSearch,
663
+ isDirectory: d.isDirectory(),
664
+ sizeBytes: size,
665
+ });
666
+ }
667
+ if (d.isDirectory()) {
668
+ traverse(fullPath, depth + 1);
669
+ }
670
+ }
671
+ };
672
+ traverse(resolvedPath, 1);
673
+ return {
674
+ pattern,
675
+ totalMatches: matches.length,
676
+ truncated,
677
+ matches,
678
+ };
679
+ }
680
+ grepFiles(query, rawPath = '.', options) {
681
+ const { resolvedPath, rootRule } = this.validatePath(rawPath, 'read');
682
+ if (!fs.existsSync(resolvedPath)) {
683
+ throw new SandboxSecurityError(`Path not found: "${rawPath}".`, 'FILE_NOT_FOUND', { path: rawPath });
684
+ }
685
+ const matches = [];
686
+ const limit = Math.min(options?.maxResults || 50, this.config.maxListEntries);
687
+ const caseInsensitive = options?.caseInsensitive ?? true;
688
+ const lowerQuery = query.toLowerCase();
689
+ let truncated = false;
690
+ const searchFile = (filePath) => {
691
+ if (matches.length >= limit) {
692
+ truncated = true;
693
+ return;
694
+ }
695
+ let content;
696
+ try {
697
+ const stat = fs.statSync(filePath);
698
+ if (stat.size > this.config.maxReadSizeBytes)
699
+ return; // Skip oversized files
700
+ // Check for binary file by reading first 512 bytes
701
+ const fd = fs.openSync(filePath, 'r');
702
+ const buf = Buffer.alloc(512);
703
+ const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
704
+ fs.closeSync(fd);
705
+ for (let i = 0; i < bytesRead; i++) {
706
+ if (buf[i] === 0)
707
+ return; // Binary file, skip
708
+ }
709
+ content = fs.readFileSync(filePath, 'utf-8');
710
+ }
711
+ catch {
712
+ return;
713
+ }
714
+ const lines = content.split(/\r?\n/);
715
+ const relFromSearch = path.relative(resolvedPath, filePath).replace(/\\/g, '/');
716
+ for (let i = 0; i < lines.length; i++) {
717
+ if (matches.length >= limit) {
718
+ truncated = true;
719
+ return;
720
+ }
721
+ const line = lines[i];
722
+ const hasMatch = caseInsensitive
723
+ ? line.toLowerCase().includes(lowerQuery)
724
+ : line.includes(query);
725
+ if (hasMatch) {
726
+ matches.push({
727
+ file: relFromSearch,
728
+ line: i + 1,
729
+ text: line.trimEnd(),
730
+ });
731
+ }
732
+ }
733
+ };
734
+ const traverse = (dir) => {
735
+ if (matches.length >= limit) {
736
+ truncated = true;
737
+ return;
738
+ }
739
+ let dirents;
740
+ try {
741
+ dirents = fs.readdirSync(dir, { withFileTypes: true });
742
+ }
743
+ catch {
744
+ return;
745
+ }
746
+ for (const d of dirents) {
747
+ if (matches.length >= limit) {
748
+ truncated = true;
749
+ return;
750
+ }
751
+ const fullPath = path.join(dir, d.name);
752
+ const relFromRoot = path.relative(rootRule.resolvedPath, fullPath).replace(/\\/g, '/');
753
+ const isBlocked = this.config.blockedPatterns.some((p) => matchGlob(d.name, p) || matchGlob(relFromRoot, p));
754
+ if (isBlocked)
755
+ continue;
756
+ if (d.isDirectory()) {
757
+ traverse(fullPath);
758
+ }
759
+ else if (d.isFile()) {
760
+ searchFile(fullPath);
761
+ }
762
+ }
763
+ };
764
+ const stat = fs.statSync(resolvedPath);
765
+ if (stat.isFile()) {
766
+ searchFile(resolvedPath);
767
+ }
768
+ else {
769
+ traverse(resolvedPath);
770
+ }
771
+ return {
772
+ query,
773
+ totalMatches: matches.length,
774
+ truncated,
775
+ matches,
776
+ };
777
+ }
778
+ }
779
+ //# sourceMappingURL=fileSandbox.js.map