@williamzujkowski/live-coding-music-mcp 4.0.0 → 4.1.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 (53) hide show
  1. package/README.md +202 -705
  2. package/dist/AudioAnalyzer.js +1294 -91
  3. package/dist/PatternStore.js +276 -23
  4. package/dist/StrudelController.js +388 -56
  5. package/dist/server/resources.js +27 -20
  6. package/dist/server/server.js +384 -76
  7. package/dist/server/tools/ai.js +124 -91
  8. package/dist/server/tools/analysis.js +168 -59
  9. package/dist/server/tools/capture.js +135 -24
  10. package/dist/server/tools/compose.js +26 -26
  11. package/dist/server/tools/diagnostics.js +20 -3
  12. package/dist/server/tools/editor.js +60 -6
  13. package/dist/server/tools/generate.js +38 -15
  14. package/dist/server/tools/history.js +30 -8
  15. package/dist/server/tools/playback.js +2 -1
  16. package/dist/server/tools/session.js +38 -6
  17. package/dist/server/tools/storage.js +179 -8
  18. package/dist/server/tools/transform.js +127 -43
  19. package/dist/server/tools/types.js +209 -14
  20. package/dist/services/AudioCaptureService.js +302 -33
  21. package/dist/services/AudioExportService.js +318 -0
  22. package/dist/services/BrowserOnlyFunctions.js +149 -0
  23. package/dist/services/EventDensityProbe.js +324 -0
  24. package/dist/services/GeminiService.js +221 -36
  25. package/dist/services/IsolatedEngineRunner.js +308 -0
  26. package/dist/services/IsolatedStrudelEngine.js +135 -0
  27. package/dist/services/LocalPatternEngine.js +16 -0
  28. package/dist/services/MIDIExportService.js +765 -71
  29. package/dist/services/MIDIImportService.js +474 -0
  30. package/dist/services/MusicTheory.js +85 -13
  31. package/dist/services/PatternGenerator.js +168 -85
  32. package/dist/services/PatternSandbox.js +359 -0
  33. package/dist/services/SessionManager.js +338 -65
  34. package/dist/services/StrudelEngine.js +218 -12
  35. package/dist/services/StrudelEngineHelpers.js +24 -8
  36. package/dist/services/StyleRegistry.js +204 -0
  37. package/dist/services/Transferable.js +114 -0
  38. package/dist/services/ai/AiTransport.js +56 -0
  39. package/dist/services/ai/AudioMeasurements.js +155 -0
  40. package/dist/services/ai/CliTransport.js +256 -0
  41. package/dist/services/engineChild.js +83 -0
  42. package/dist/services/engineChildPath.js +28 -0
  43. package/dist/utils/CategorisedError.js +59 -0
  44. package/dist/utils/ErrorRecovery.js +130 -110
  45. package/dist/utils/InputValidator.js +55 -7
  46. package/dist/utils/PatternValidator.js +136 -17
  47. package/dist/utils/SafePath.js +112 -0
  48. package/dist/utils/ServerConfig.js +248 -0
  49. package/dist/utils/TableLookup.js +41 -0
  50. package/dist/utils/Tempo.js +143 -0
  51. package/package.json +17 -5
  52. package/dist/services/__mocks__/StrudelEngine.js +0 -157
  53. package/dist/types/index.js +0 -1
@@ -0,0 +1,359 @@
1
+ /**
2
+ * PatternSandbox - containment for locally-executed Strudel pattern code
3
+ *
4
+ * `StrudelEngine` has to *run* pattern code to validate it or query its
5
+ * events: a pattern is JavaScript that builds a Pattern object, and there
6
+ * is no way to know what it produces without evaluating it.
7
+ *
8
+ * It used to do that with `new Function(...)` directly in the server
9
+ * process (#229). That is arbitrary code execution: the Strudel transpiler
10
+ * is an acorn-based source-to-source rewriter, not a sandbox — it turns
11
+ * double-quoted strings into `mini()` calls and passes every other
12
+ * statement through untouched. `validate_pattern_local` also runs without
13
+ * `init`, so the whole path was reachable with no browser and no session.
14
+ *
15
+ * Two layers guard it now:
16
+ *
17
+ * 1. `assertPatternIsSafe()` — an acorn AST allowlist over the *source*.
18
+ * Free identifiers must be Strudel functions, a small set of safe
19
+ * builtins, or something the code declared itself. Member access to
20
+ * `constructor` / `__proto__` / `prototype` is rejected, as is
21
+ * dynamic member access with a computed key.
22
+ *
23
+ * 2. `runPatternCode()` — `node:vm` with code generation disabled and a
24
+ * wall-clock timeout.
25
+ *
26
+ * Layer 2 is deliberately *not* the primary control. `node:vm` is not a
27
+ * security boundary: the sandbox receives real host functions, and
28
+ * `someHostFn.constructor.constructor('return process')()` compiles in the
29
+ * main realm, reaching `process` straight through the context wall. That
30
+ * escape is verified to work, which is exactly why layer 1 bans
31
+ * `.constructor` at the syntax level and never lets the expression form.
32
+ *
33
+ * @module services/PatternSandbox
34
+ * @nist si-10 "Information input validation"
35
+ * @nist sc-39 "Process isolation"
36
+ */
37
+ import { parse } from 'acorn';
38
+ import { createContext, runInContext } from 'node:vm';
39
+ /** Wall-clock budget for a single pattern evaluation. */
40
+ export const EXECUTION_TIMEOUT_MS = 1000;
41
+ /**
42
+ * Property names that expose a route out of the sandbox.
43
+ *
44
+ * `constructor` is the important one: from any host function it reaches
45
+ * the real `Function` constructor, which compiles in the main realm.
46
+ */
47
+ const BANNED_PROPERTIES = new Set([
48
+ 'constructor',
49
+ '__proto__',
50
+ 'prototype',
51
+ '__defineGetter__',
52
+ '__defineSetter__',
53
+ '__lookupGetter__',
54
+ '__lookupSetter__',
55
+ ]);
56
+ /**
57
+ * Names a binding may not introduce, however it is declared.
58
+ *
59
+ * `collectDeclaredNames` is scope-blind on purpose — it builds one
60
+ * whole-file set, so a name declared anywhere makes bare references to
61
+ * it legal everywhere. That is fine for `const kick = ...`, and it was
62
+ * a hole for anything the sandbox deliberately withholds:
63
+ *
64
+ * function bypass(Reflect) {} // never called
65
+ * Reflect.get(...) // now a legal identifier
66
+ *
67
+ * Measured: with that dead parameter present, bare `Reflect`, `Object`,
68
+ * `globalThis`, `Function` and `eval` all became referenceable, and each
69
+ * resolves at runtime to the vm context's real intrinsic — not to the
70
+ * unused parameter. `SAFE_GLOBALS` omits `Object` and `Reflect`
71
+ * deliberately; a parameter name was undoing that decision.
72
+ *
73
+ * The same rule `BANNED_PROPERTIES` already gets: a binding named after
74
+ * something withheld does not make it available. Proper lexical scoping
75
+ * would be the general fix; this closes the measured hole without
76
+ * rewriting the checker into a scope analyser.
77
+ *
78
+ * No demonstrated escape came out of it — `Function` and `eval` inside
79
+ * the context are dead under `codeGeneration: { strings: false }`, and
80
+ * `process` and `require` are absent — but this is the layer whose job
81
+ * is to stop the attempt, and failing to build a chain is not proof
82
+ * that none exists.
83
+ */
84
+ const BANNED_BINDINGS = new Set([
85
+ 'Object', 'Reflect', 'Proxy', 'Function', 'eval',
86
+ 'globalThis', 'global', 'process', 'require', 'module', 'exports',
87
+ 'import', 'WeakRef', 'FinalizationRegistry', 'Symbol', 'Promise',
88
+ ]);
89
+ /**
90
+ * Globals a pattern may legitimately reference beyond the Strudel context.
91
+ *
92
+ * Deliberately excludes `Object` and `Reflect` — both walk prototype
93
+ * chains, and while BANNED_PROPERTIES blocks the obvious follow-up, there
94
+ * is no reason a pattern needs them.
95
+ */
96
+ const SAFE_GLOBALS = new Set([
97
+ 'Math', 'Number', 'String', 'Boolean', 'Array', 'JSON', 'Date',
98
+ 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
99
+ 'NaN', 'Infinity', 'undefined',
100
+ ]);
101
+ /**
102
+ * Largest numeric literal a pattern may contain.
103
+ *
104
+ * A fast-fail for accidents and naive input, NOT a security control.
105
+ * `Array(50000000).fill(7)` reached 1908 MB in 5791 ms before the vm's
106
+ * 1000 ms timeout fired — V8 cannot interrupt inside an allocating
107
+ * builtin, so a wall-clock budget is not a memory budget (#307).
108
+ *
109
+ * A determined payload evades this trivially by computing the number
110
+ * (`Array(1e4 * 5e3)`), and this deliberately does not try to constant-
111
+ * fold arithmetic — chasing that is an arms race an AST walk loses. The
112
+ * real containment is process isolation with a hard heap cap, settled
113
+ * by measurement on #307: worker_threads with `resourceLimits` does NOT
114
+ * contain the OOM (the parent dumped core), a forked child with
115
+ * `--max-old-space-size` does.
116
+ *
117
+ * What this buys, honestly: the common accidental case fails in
118
+ * microseconds with a message naming the problem, instead of allocating
119
+ * hundreds of megabytes first.
120
+ *
121
+ * The bound is far above anything musical — a pattern asking for a
122
+ * million of anything is not describing music.
123
+ */
124
+ const MAX_NUMERIC_LITERAL = 1_000_000;
125
+ /** AST node types a pattern is allowed to contain. */
126
+ const ALLOWED_NODE_TYPES = new Set([
127
+ 'Program', 'ExpressionStatement', 'CallExpression', 'MemberExpression',
128
+ 'Identifier', 'Literal', 'TemplateLiteral', 'TemplateElement',
129
+ 'TaggedTemplateExpression', 'ArrayExpression', 'ObjectExpression',
130
+ 'Property', 'ArrowFunctionExpression', 'FunctionExpression',
131
+ 'FunctionDeclaration', 'BlockStatement', 'ReturnStatement',
132
+ 'VariableDeclaration', 'VariableDeclarator', 'BinaryExpression',
133
+ 'UnaryExpression', 'LogicalExpression', 'ConditionalExpression',
134
+ 'SpreadElement', 'RestElement', 'AssignmentExpression', 'UpdateExpression',
135
+ 'ArrayPattern', 'ObjectPattern', 'AssignmentPattern', 'SequenceExpression',
136
+ // No 'ParenthesizedExpression': acorn only produces that node with
137
+ // `preserveParens: true`, which this parser does not pass, so the
138
+ // entry never matched anything. Verified — `(note("c"))` parses to
139
+ // Program/ExpressionStatement/CallExpression/Identifier/Literal (#491).
140
+ 'ChainExpression', 'EmptyStatement',
141
+ 'IfStatement', 'ForStatement', 'ForOfStatement', 'WhileStatement',
142
+ 'BreakStatement', 'ContinueStatement',
143
+ ]);
144
+ /** Raised when a pattern is rejected before execution. */
145
+ export class PatternSafetyError extends Error {
146
+ constructor(message) {
147
+ super(message);
148
+ this.name = 'PatternSafetyError';
149
+ }
150
+ }
151
+ /** Walks every child node of an AST node. */
152
+ function* childNodes(node) {
153
+ for (const key of Object.keys(node)) {
154
+ if (key === 'type' || key === 'start' || key === 'end' || key === 'loc')
155
+ continue;
156
+ const value = node[key];
157
+ if (Array.isArray(value)) {
158
+ for (const item of value) {
159
+ if (item && typeof item.type === 'string')
160
+ yield item;
161
+ }
162
+ }
163
+ else if (value && typeof value.type === 'string') {
164
+ yield value;
165
+ }
166
+ }
167
+ }
168
+ /** Collects every name the code binds, so locals aren't mistaken for globals. */
169
+ function collectDeclaredNames(ast) {
170
+ const declared = new Set();
171
+ const addPattern = (node) => {
172
+ if (!node || typeof node.type !== 'string')
173
+ return;
174
+ switch (node.type) {
175
+ case 'Identifier':
176
+ // A binding named after a banned property — or after a global
177
+ // the sandbox withholds — would otherwise make later bare
178
+ // references to it legal anywhere in the file.
179
+ if (!BANNED_PROPERTIES.has(node.name) && !BANNED_BINDINGS.has(node.name)) {
180
+ declared.add(node.name);
181
+ }
182
+ break;
183
+ case 'ObjectPattern':
184
+ for (const prop of node.properties ?? []) {
185
+ addPattern(prop.type === 'Property' ? prop.value : prop.argument);
186
+ }
187
+ break;
188
+ case 'ArrayPattern':
189
+ for (const el of node.elements ?? [])
190
+ addPattern(el);
191
+ break;
192
+ case 'AssignmentPattern':
193
+ addPattern(node.left);
194
+ break;
195
+ case 'RestElement':
196
+ addPattern(node.argument);
197
+ break;
198
+ default:
199
+ break;
200
+ }
201
+ };
202
+ const walk = (node) => {
203
+ if (node.type === 'VariableDeclarator')
204
+ addPattern(node.id);
205
+ if (node.type === 'FunctionDeclaration' ||
206
+ node.type === 'FunctionExpression' ||
207
+ node.type === 'ArrowFunctionExpression') {
208
+ if (node.id)
209
+ addPattern(node.id);
210
+ for (const param of node.params ?? [])
211
+ addPattern(param);
212
+ }
213
+ for (const child of childNodes(node))
214
+ walk(child);
215
+ };
216
+ walk(ast);
217
+ return declared;
218
+ }
219
+ /**
220
+ * Rejects pattern source that could escape the sandbox.
221
+ *
222
+ * @param code - Strudel pattern source, before transpilation
223
+ * @param allowedGlobals - Names provided by the Strudel execution context
224
+ * @throws {PatternSafetyError} When the pattern is not safe to execute
225
+ * @nist si-10 "Information input validation"
226
+ */
227
+ export function assertPatternIsSafe(code, allowedGlobals) {
228
+ let ast;
229
+ try {
230
+ ast = parse(code, { ecmaVersion: 2022, sourceType: 'script' });
231
+ }
232
+ catch (error) {
233
+ // Let the transpiler produce the friendly syntax-error message; this
234
+ // path only needs to refuse to execute unparseable input.
235
+ throw new PatternSafetyError(`Pattern could not be parsed: ${error.message}`);
236
+ }
237
+ const declared = collectDeclaredNames(ast);
238
+ const allowed = new Set([...allowedGlobals, ...SAFE_GLOBALS, ...declared]);
239
+ const walk = (node, parent) => {
240
+ if (!ALLOWED_NODE_TYPES.has(node.type)) {
241
+ throw new PatternSafetyError(`Pattern contains disallowed syntax (${node.type}). ` +
242
+ 'Patterns may only build and combine Strudel values.');
243
+ }
244
+ // Destructuring reads a property with no MemberExpression node at
245
+ // all — `const { constructor: C } = note` binds the same function
246
+ // that `note.constructor` would, and the check below never sees it.
247
+ // collectDeclaredNames then added `C` to the allowed set, so the
248
+ // identifier check passed too. Two holes lining up into a sandbox
249
+ // escape (#SANDBOX).
250
+ // Cheap upper bound on allocation-shaped input. See
251
+ // MAX_NUMERIC_LITERAL: a fast-fail, not a control.
252
+ // `bigint` counts too.
253
+ //
254
+ // This tested `typeof node.value === 'number'` only, so a bigint
255
+ // literal walked straight past it: `Array(Number(50000000n)).fill(7)`
256
+ // was accepted by the AST check. Measured, the isolated child's heap
257
+ // cap caught it and returned a clean error, so it was contained
258
+ // rather than an escape — but a fast-fail that any `n` suffix
259
+ // sidesteps is not the fast-fail it claims to be (#491).
260
+ if (node.type === 'Literal'
261
+ && (typeof node.value === 'number' || typeof node.value === 'bigint')) {
262
+ const magnitude = typeof node.value === 'bigint'
263
+ ? (node.value < 0n ? -node.value : node.value)
264
+ : Math.abs(node.value);
265
+ const overLimit = typeof magnitude === 'bigint'
266
+ ? magnitude > BigInt(MAX_NUMERIC_LITERAL)
267
+ : Number.isFinite(magnitude) && magnitude > MAX_NUMERIC_LITERAL;
268
+ if (overLimit) {
269
+ throw new PatternSafetyError(`Pattern contains the number ${String(node.value)}, above the ` +
270
+ `${String(MAX_NUMERIC_LITERAL)} limit. Numbers that large usually mean ` +
271
+ 'an allocation mistake rather than a musical intent.');
272
+ }
273
+ }
274
+ if (node.type === 'ObjectPattern') {
275
+ for (const prop of node.properties ?? []) {
276
+ if (prop.type !== 'Property')
277
+ continue;
278
+ const key = prop.key;
279
+ const name = prop.computed
280
+ ? (key?.type === 'Literal' ? String(key.value) : null)
281
+ : (key?.name ?? (key?.type === 'Literal' ? String(key.value) : null));
282
+ if (name === null) {
283
+ throw new PatternSafetyError('Pattern destructures a computed property name, which is not allowed. ' +
284
+ 'Use a literal property name.');
285
+ }
286
+ if (BANNED_PROPERTIES.has(name)) {
287
+ throw new PatternSafetyError(`Pattern destructures the disallowed property '${name}'.`);
288
+ }
289
+ }
290
+ }
291
+ // An object literal with a computed key was never inspected, so
292
+ // `{ ["constructor"]: … }` passed. Nothing here needs computed keys.
293
+ if (node.type === 'Property' && node.computed && parent?.type !== 'ObjectPattern') {
294
+ const key = node.key;
295
+ const name = key?.type === 'Literal' ? String(key.value) : null;
296
+ if (name === null || BANNED_PROPERTIES.has(name)) {
297
+ throw new PatternSafetyError('Pattern uses a computed object key, which is not allowed. ' +
298
+ 'Use a literal property name.');
299
+ }
300
+ }
301
+ if (node.type === 'MemberExpression') {
302
+ if (node.computed) {
303
+ // `x["constructor"]` and `x[name]` both die here; a literal index
304
+ // like `[1,2,3][0]` is still fine.
305
+ if (node.property.type === 'Literal') {
306
+ if (BANNED_PROPERTIES.has(String(node.property.value))) {
307
+ throw new PatternSafetyError(`Pattern accesses the disallowed property '${String(node.property.value)}'.`);
308
+ }
309
+ }
310
+ else {
311
+ throw new PatternSafetyError('Pattern uses computed property access, which is not allowed. ' +
312
+ 'Use a literal property name.');
313
+ }
314
+ }
315
+ else if (BANNED_PROPERTIES.has(node.property.name)) {
316
+ throw new PatternSafetyError(`Pattern accesses the disallowed property '${node.property.name}'.`);
317
+ }
318
+ }
319
+ // Identifiers in a *reference* position must resolve to something known.
320
+ // Property names, object keys, and binding sites are not references.
321
+ if (node.type === 'Identifier' && parent) {
322
+ const isPropertyName = parent.type === 'MemberExpression' && parent.property === node && !parent.computed;
323
+ const isObjectKey = parent.type === 'Property' && parent.key === node && !parent.computed;
324
+ if (!isPropertyName && !isObjectKey && !allowed.has(node.name)) {
325
+ throw new PatternSafetyError(`Pattern references unknown identifier '${node.name}'. ` +
326
+ 'Only Strudel functions and locally declared names are available.');
327
+ }
328
+ }
329
+ for (const child of childNodes(node))
330
+ walk(child, node);
331
+ };
332
+ walk(ast, null);
333
+ }
334
+ /**
335
+ * Runs transpiled pattern code against the Strudel context.
336
+ *
337
+ * Defense in depth behind `assertPatternIsSafe`. Code generation is
338
+ * disabled so `eval` and `Function` throw inside the context, and the
339
+ * timeout bounds a runaway pattern.
340
+ *
341
+ * @param transpiledCode - Output of the Strudel transpiler
342
+ * @param context - Strudel functions to expose
343
+ * @param timeoutMs - Wall-clock budget
344
+ * @returns Whatever the pattern code evaluates to
345
+ * @nist sc-39 "Process isolation"
346
+ */
347
+ export function runPatternCode(transpiledCode, context, timeoutMs = EXECUTION_TIMEOUT_MS) {
348
+ const sandbox = Object.create(null);
349
+ for (const [key, value] of Object.entries(context))
350
+ sandbox[key] = value;
351
+ const vmContext = createContext(sandbox, {
352
+ codeGeneration: { strings: false, wasm: false },
353
+ });
354
+ return runInContext(`(function(){${transpiledCode}\n})()`, vmContext, {
355
+ timeout: timeoutMs,
356
+ // No importModuleDynamically callback: dynamic import throws
357
+ // ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING rather than resolving.
358
+ });
359
+ }