@marifold/core 0.64.1 → 0.66.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 (77) hide show
  1. package/dist/agent/AgentRunner.d.ts +7 -0
  2. package/dist/agent/AgentRunner.d.ts.map +1 -1
  3. package/dist/agent/AgentRunner.js +27 -1
  4. package/dist/agent/AgentRunner.js.map +1 -1
  5. package/dist/agent/RunWorkspace.d.ts +10 -0
  6. package/dist/agent/RunWorkspace.d.ts.map +1 -1
  7. package/dist/agent/RunWorkspace.js +19 -1
  8. package/dist/agent/RunWorkspace.js.map +1 -1
  9. package/dist/agent/tools/ReadFileTool.d.ts +7 -0
  10. package/dist/agent/tools/ReadFileTool.d.ts.map +1 -1
  11. package/dist/agent/tools/ReadFileTool.js +24 -2
  12. package/dist/agent/tools/ReadFileTool.js.map +1 -1
  13. package/dist/app/AppStore.d.ts +11 -1
  14. package/dist/app/AppStore.d.ts.map +1 -1
  15. package/dist/app/AppStore.js +105 -16
  16. package/dist/app/AppStore.js.map +1 -1
  17. package/dist/app/SkillAppCompiler.d.ts.map +1 -1
  18. package/dist/app/SkillAppCompiler.js +314 -44
  19. package/dist/app/SkillAppCompiler.js.map +1 -1
  20. package/dist/app/SkillAppDsl.d.ts +60 -4
  21. package/dist/app/SkillAppDsl.d.ts.map +1 -1
  22. package/dist/app/SkillAppDsl.js +44 -0
  23. package/dist/app/SkillAppDsl.js.map +1 -1
  24. package/dist/app/SkillAppInstanceRegistry.d.ts +3 -2
  25. package/dist/app/SkillAppInstanceRegistry.d.ts.map +1 -1
  26. package/dist/app/SkillAppInstanceRegistry.js +191 -11
  27. package/dist/app/SkillAppInstanceRegistry.js.map +1 -1
  28. package/dist/app/SkillAppResolver.d.ts +5 -2
  29. package/dist/app/SkillAppResolver.d.ts.map +1 -1
  30. package/dist/app/SkillAppResolver.js +66 -15
  31. package/dist/app/SkillAppResolver.js.map +1 -1
  32. package/dist/app/SkillAppSchema.d.ts +84 -8
  33. package/dist/app/SkillAppSchema.d.ts.map +1 -1
  34. package/dist/app/SkillAppSchema.js +2 -1
  35. package/dist/app/SkillAppSchema.js.map +1 -1
  36. package/dist/config/ConfigLoader.d.ts.map +1 -1
  37. package/dist/config/ConfigLoader.js +17 -3
  38. package/dist/config/ConfigLoader.js.map +1 -1
  39. package/dist/config/ConfigManager.d.ts.map +1 -1
  40. package/dist/config/ConfigManager.js +13 -2
  41. package/dist/config/ConfigManager.js.map +1 -1
  42. package/dist/config/ConfigSchema.d.ts +8 -2
  43. package/dist/config/ConfigSchema.d.ts.map +1 -1
  44. package/dist/config/ConfigSchema.js.map +1 -1
  45. package/dist/config/MarifoldOpenAICompatProvider.d.ts +1 -1
  46. package/dist/config/MarifoldOpenAICompatProvider.d.ts.map +1 -1
  47. package/dist/config/MarifoldOpenAICompatProvider.js +8 -6
  48. package/dist/config/MarifoldOpenAICompatProvider.js.map +1 -1
  49. package/dist/config/ProviderFactory.d.ts +6 -4
  50. package/dist/config/ProviderFactory.d.ts.map +1 -1
  51. package/dist/config/ProviderFactory.js +41 -6
  52. package/dist/config/ProviderFactory.js.map +1 -1
  53. package/dist/index.d.ts +8 -6
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +14 -3
  56. package/dist/index.js.map +1 -1
  57. package/dist/runtime/MarifoldRuntime.d.ts +17 -9
  58. package/dist/runtime/MarifoldRuntime.d.ts.map +1 -1
  59. package/dist/runtime/MarifoldRuntime.js +270 -59
  60. package/dist/runtime/MarifoldRuntime.js.map +1 -1
  61. package/dist/runtime/MarifoldTypes.d.ts +1 -1
  62. package/dist/runtime/MarifoldTypes.d.ts.map +1 -1
  63. package/dist/runtime/NativeWebSearch.d.ts +9 -0
  64. package/dist/runtime/NativeWebSearch.d.ts.map +1 -0
  65. package/dist/runtime/NativeWebSearch.js +17 -0
  66. package/dist/runtime/NativeWebSearch.js.map +1 -0
  67. package/dist/search/OllamaSearchBackend.d.ts +22 -0
  68. package/dist/search/OllamaSearchBackend.d.ts.map +1 -0
  69. package/dist/search/OllamaSearchBackend.js +64 -0
  70. package/dist/search/OllamaSearchBackend.js.map +1 -0
  71. package/dist/search/createSearchBackend.d.ts +4 -3
  72. package/dist/search/createSearchBackend.d.ts.map +1 -1
  73. package/dist/search/createSearchBackend.js +13 -3
  74. package/dist/search/createSearchBackend.js.map +1 -1
  75. package/dist/skill/SkillStore.d.ts +2 -2
  76. package/dist/skill/SkillStore.js +2 -2
  77. package/package.json +1 -1
@@ -42,9 +42,12 @@ const SAFE_APP_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
42
42
  const SAFE_LOCAL_NAME = /^[a-zA-Z][a-zA-Z0-9_]*$/;
43
43
  const SAFE_SKILL_NAME = /^[a-z0-9][a-z0-9_-]*$/;
44
44
  const SAFE_PROVIDER_NAME = /^[a-z0-9][a-z0-9_-]*$/;
45
+ const SAFE_PROFILE_NAME = /^[A-Za-z0-9_-]+$/;
45
46
  const ALLOWED_BUILDERS = new Set([
46
- 'App', 'Button', 'Column', 'Row', 'Select', 'Spacer', 'State', 'Textarea',
47
- 'TextResult', 'defineSkillApp', 'registerModel', 'registerSkill', 'trigger', 'useSkill',
47
+ 'App', 'AttachmentState', 'Attachments', 'Button', 'Column', 'FileAccess',
48
+ 'FolderAccess', 'Row', 'Select', 'Spacer', 'State', 'Textarea',
49
+ 'TextResult', 'defineSkillApp', 'registerModel', 'registerProfile', 'registerSkill',
50
+ 'trigger', 'useProfileSkill', 'useSkill',
48
51
  ]);
49
52
  /** Compile the restricted TypeScript authoring syntax into renderer-neutral data.
50
53
  * The source is inspected as an AST and is never evaluated or imported. */
@@ -63,7 +66,9 @@ function compileSkillApp(source, sourcePath = 'skillapp.ts') {
63
66
  imports: new Map(),
64
67
  values: new Map(),
65
68
  states: [],
69
+ attachmentStates: [],
66
70
  models: [],
71
+ profiles: [],
67
72
  skills: [],
68
73
  operations: [],
69
74
  triggers: [],
@@ -89,6 +94,8 @@ function compileSkillApp(source, sourcePath = 'skillapp.ts') {
89
94
  const tagged = requireTagged(value, 'skillapp', statement.expression, state, sourcePath);
90
95
  state.template = {
91
96
  app: requireObject(tagged.app, 'defineSkillApp app', statement.expression, state, sourcePath),
97
+ permissions: requireArray(tagged.permissions ?? [], 'defineSkillApp permissions', statement.expression, state, sourcePath)
98
+ .map(permission => requireTagged(permission, 'permission', statement.expression, state, sourcePath)),
92
99
  ui: requireTagged(tagged.ui, 'component', statement.expression, state, sourcePath),
93
100
  };
94
101
  }
@@ -99,16 +106,20 @@ function compileSkillApp(source, sourcePath = 'skillapp.ts') {
99
106
  if (!state.template)
100
107
  throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must export default defineSkillApp({...}).', sourcePath);
101
108
  const app = normalizeAppInfo(state.template.app, state, sourcePath);
109
+ const permissions = normalizePermissions(state.template.permissions, state, sourcePath);
102
110
  const layoutRoot = normalizeComponent(state.template.ui, state, sourcePath);
103
111
  if (layoutRoot.component !== 'app') {
104
112
  throw MarifoldError_1.MarifoldError.appInvalid('defineSkillApp.ui must be App([...]).', sourcePath);
105
113
  }
106
114
  validateReferences(state, layoutRoot.children ?? [], sourcePath);
107
115
  return {
108
- schema: SkillAppSchema_1.SKILL_APP_SCHEMA,
116
+ schema: state.profiles.length > 0 ? SkillAppSchema_1.SKILL_APP_PROFILE_SCHEMA : SkillAppSchema_1.SKILL_APP_SCHEMA,
109
117
  app,
110
118
  states: state.states,
119
+ attachmentStates: state.attachmentStates,
120
+ permissions,
111
121
  models: state.models,
122
+ profiles: state.profiles,
112
123
  skills: state.skills,
113
124
  operations: state.operations,
114
125
  triggers: state.triggers,
@@ -156,20 +167,12 @@ function registerDeclaration(name, value, node, state, sourcePath) {
156
167
  if (value.__kind === 'state') {
157
168
  state.states.push({ name, initial: requireString(value.initial, 'State initial value', node, state, sourcePath) });
158
169
  }
170
+ else if (value.__kind === 'attachment_state') {
171
+ state.attachmentStates.push({ name });
172
+ }
159
173
  else if (value.__kind === 'model') {
160
174
  const id = requireString(value.id, 'model id', node, state, sourcePath);
161
- const separator = id.indexOf('/');
162
- if (separator <= 0 || separator === id.length - 1) {
163
- throw invalidAt(state.sourceFile, node.pos, `Model '${id}' must use provider/model format.`, sourcePath);
164
- }
165
- const provider = id.slice(0, separator);
166
- if (!SAFE_PROVIDER_NAME.test(provider)) {
167
- throw invalidAt(state.sourceFile, node.pos, `Invalid model provider '${provider}'.`, sourcePath);
168
- }
169
- const model = id.slice(separator + 1);
170
- if (model.trim() !== model || model.length === 0) {
171
- throw invalidAt(state.sourceFile, node.pos, `Invalid model id '${model}'.`, sourcePath);
172
- }
175
+ const { provider, model } = parseModelId(id, node, state, sourcePath);
173
176
  state.models.push({
174
177
  name,
175
178
  provider,
@@ -177,6 +180,28 @@ function registerDeclaration(name, value, node, state, sourcePath) {
177
180
  think: requireBoolean(value.think, 'model think', node, state, sourcePath),
178
181
  });
179
182
  }
183
+ else if (value.__kind === 'profile') {
184
+ const profile = requireString(value.profileName, 'profile name', node, state, sourcePath);
185
+ if (!SAFE_PROFILE_NAME.test(profile)) {
186
+ throw invalidAt(state.sourceFile, node.pos, `Invalid profile name '${profile}'.`, sourcePath);
187
+ }
188
+ const modelId = value.modelId === undefined
189
+ ? undefined
190
+ : requireString(value.modelId, 'profile model override', node, state, sourcePath);
191
+ const modelOverride = modelId === undefined
192
+ ? undefined
193
+ : parseModelId(modelId, node, state, sourcePath);
194
+ state.profiles.push({
195
+ name,
196
+ profile,
197
+ ...(modelOverride ?? {}),
198
+ ...(value.think !== undefined
199
+ ? { think: requireBoolean(value.think, 'profile think', node, state, sourcePath) }
200
+ : {}),
201
+ memory: requireBoolean(value.memory, 'profile memory', node, state, sourcePath),
202
+ history: requireBoolean(value.history, 'profile history', node, state, sourcePath),
203
+ });
204
+ }
180
205
  else if (value.__kind === 'skill') {
181
206
  const skillName = requireString(value.skillName, 'skill name', node, state, sourcePath);
182
207
  if (!SAFE_SKILL_NAME.test(skillName)) {
@@ -189,18 +214,62 @@ function registerDeclaration(name, value, node, state, sourcePath) {
189
214
  });
190
215
  }
191
216
  else if (value.__kind === 'operation') {
192
- state.operations.push({
193
- name,
194
- model: requireNamedReference(value.model, 'model', node, state, sourcePath),
195
- skill: requireNamedReference(value.skill, 'skill', node, state, sourcePath, true),
196
- parameters: Object.fromEntries(Object.entries(requireObject(value.parameters, 'operation parameters', node, state, sourcePath)).map(([parameter, reference]) => [
197
- parameter,
198
- requireNamedReference(reference, 'state', node, state, sourcePath),
199
- ])),
200
- requiredInputs: [],
201
- output: requireNamedReference(value.output, 'state', node, state, sourcePath),
202
- execution: { memory: false, history: false, profileContext: false },
203
- });
217
+ const parameters = Object.fromEntries(Object.entries(requireObject(value.parameters, 'operation parameters', node, state, sourcePath)).map(([parameter, reference]) => [
218
+ parameter,
219
+ requireNamedReference(reference, 'state', node, state, sourcePath),
220
+ ]));
221
+ if (value.profile !== undefined) {
222
+ const profile = requireTagged(value.profile, 'profile', node, state, sourcePath);
223
+ const result = requireTagged(value.result, 'text_result', node, state, sourcePath);
224
+ const skillName = value.skillName === undefined
225
+ ? undefined
226
+ : requireString(value.skillName, 'profile skill name', node, state, sourcePath);
227
+ const skillState = value.skillState === undefined
228
+ ? undefined
229
+ : requireNamedReference(value.skillState, 'state', node, state, sourcePath);
230
+ const skillOptions = value.skillOptions === undefined
231
+ ? undefined
232
+ : requireSkillOptionValues(value.skillOptions, node, state, sourcePath);
233
+ state.operations.push({
234
+ name,
235
+ profile: requireName(profile, 'profile', node, state, sourcePath),
236
+ ...(skillName ? { skill: skillName } : {}),
237
+ ...(skillState ? { skillState } : {}),
238
+ ...(skillOptions ? { skillOptions } : {}),
239
+ ...(value.stripSkillName !== undefined
240
+ ? { stripSkillName: requireBoolean(value.stripSkillName, 'stripSkillName', node, state, sourcePath) }
241
+ : {}),
242
+ ...(value.input !== undefined
243
+ ? { input: requireNamedReference(value.input, 'state', node, state, sourcePath) }
244
+ : {}),
245
+ ...(value.attachments !== undefined
246
+ ? { attachments: requireNamedReference(value.attachments, 'attachment_state', node, state, sourcePath) }
247
+ : {}),
248
+ parameters,
249
+ requiredInputs: [],
250
+ output: requireNamedReference(value.output, 'state', node, state, sourcePath),
251
+ result: {
252
+ kind: 'text',
253
+ trim: requireBoolean(result.trim, 'result trim', node, state, sourcePath),
254
+ },
255
+ execution: {
256
+ memory: requireBoolean(profile.memory, 'profile memory', node, state, sourcePath),
257
+ history: requireBoolean(profile.history, 'profile history', node, state, sourcePath),
258
+ profileContext: true,
259
+ },
260
+ });
261
+ }
262
+ else {
263
+ state.operations.push({
264
+ name,
265
+ model: requireNamedReference(value.model, 'model', node, state, sourcePath),
266
+ skill: requireNamedReference(value.skill, 'skill', node, state, sourcePath, true),
267
+ parameters,
268
+ requiredInputs: [],
269
+ output: requireNamedReference(value.output, 'state', node, state, sourcePath),
270
+ execution: { memory: false, history: false, profileContext: false },
271
+ });
272
+ }
204
273
  }
205
274
  }
206
275
  function evaluateExpression(rawNode, state, sourcePath, declarationName) {
@@ -269,6 +338,25 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
269
338
  case 'State':
270
339
  exactArgs(name, args, 1, node, state, sourcePath);
271
340
  return { __kind: 'state', initial: requireString(args[0], 'State initial value', node, state, sourcePath) };
341
+ case 'AttachmentState':
342
+ exactArgs(name, args, 0, node, state, sourcePath);
343
+ return { __kind: 'attachment_state' };
344
+ case 'FileAccess':
345
+ case 'FolderAccess': {
346
+ exactArgs(name, args, 2, node, state, sourcePath);
347
+ const options = requireObject(args[1], `${name} options`, node, state, sourcePath);
348
+ rejectUnknown(options, ['access'], name, node, state, sourcePath);
349
+ const access = requireString(options.access, `${name} access`, node, state, sourcePath);
350
+ if (access !== 'read') {
351
+ throw invalidAt(state.sourceFile, node.pos, `${name} access must be "read".`, sourcePath);
352
+ }
353
+ return {
354
+ __kind: 'permission',
355
+ resource: name === 'FileAccess' ? 'file' : 'folder',
356
+ path: requireNonEmptyString(args[0], `${name} path`, node, state, sourcePath),
357
+ access,
358
+ };
359
+ }
272
360
  case 'registerModel': {
273
361
  argsRange(name, args, 1, 2, node, state, sourcePath);
274
362
  const options = args[1] === undefined ? {} : requireObject(args[1], 'registerModel options', node, state, sourcePath);
@@ -279,6 +367,23 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
279
367
  think: options.think === undefined ? false : requireBoolean(options.think, 'think', node, state, sourcePath),
280
368
  };
281
369
  }
370
+ case 'registerProfile': {
371
+ argsRange(name, args, 1, 2, node, state, sourcePath);
372
+ const options = args[1] === undefined ? {} : requireObject(args[1], 'registerProfile options', node, state, sourcePath);
373
+ rejectUnknown(options, ['model', 'think', 'memory', 'history'], name, node, state, sourcePath);
374
+ return {
375
+ __kind: 'profile',
376
+ profileName: requireString(args[0], 'profile name', node, state, sourcePath),
377
+ ...(options.model !== undefined
378
+ ? { modelId: requireString(options.model, 'model', node, state, sourcePath) }
379
+ : {}),
380
+ ...(options.think !== undefined
381
+ ? { think: requireBoolean(options.think, 'think', node, state, sourcePath) }
382
+ : {}),
383
+ memory: options.memory === undefined ? false : requireBoolean(options.memory, 'memory', node, state, sourcePath),
384
+ history: options.history === undefined ? false : requireBoolean(options.history, 'history', node, state, sourcePath),
385
+ };
386
+ }
282
387
  case 'TextResult': {
283
388
  argsRange(name, args, 0, 1, node, state, sourcePath);
284
389
  const options = args[0] === undefined ? {} : requireObject(args[0], 'TextResult options', node, state, sourcePath);
@@ -314,6 +419,45 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
314
419
  output: requireTagged(options.output, 'state', node, state, sourcePath),
315
420
  };
316
421
  }
422
+ case 'useProfileSkill': {
423
+ exactArgs(name, args, 3, node, state, sourcePath);
424
+ const profile = requireTagged(args[0], 'profile', node, state, sourcePath);
425
+ const skillName = typeof args[1] === 'string' ? args[1] : undefined;
426
+ const skillState = skillName === undefined
427
+ ? requireTagged(args[1], 'state', node, state, sourcePath)
428
+ : undefined;
429
+ if (skillName !== undefined && !SAFE_SKILL_NAME.test(skillName)) {
430
+ throw invalidAt(state.sourceFile, node.pos, `Invalid profile skill name '${skillName}'.`, sourcePath);
431
+ }
432
+ const options = requireObject(args[2], 'useProfileSkill options', node, state, sourcePath);
433
+ rejectUnknown(options, ['skills', 'input', 'attachments', 'stripSkillName', 'parameters', 'output', 'result'], name, node, state, sourcePath);
434
+ if (skillName !== undefined && options.skills !== undefined) {
435
+ throw invalidAt(state.sourceFile, node.pos, 'useProfileSkill.skills is only valid when the Skill is selected by State.', sourcePath);
436
+ }
437
+ if (skillState !== undefined && options.skills === undefined) {
438
+ throw invalidAt(state.sourceFile, node.pos, 'A state-selected profile Skill requires a non-empty useProfileSkill.skills allowlist.', sourcePath);
439
+ }
440
+ return {
441
+ __kind: 'operation',
442
+ profile,
443
+ ...(skillName !== undefined ? { skillName } : { skillState }),
444
+ ...(options.skills !== undefined ? { skillOptions: options.skills } : {}),
445
+ ...(options.input !== undefined
446
+ ? { input: requireTagged(options.input, 'state', node, state, sourcePath) }
447
+ : {}),
448
+ ...(options.attachments !== undefined
449
+ ? { attachments: requireTagged(options.attachments, 'attachment_state', node, state, sourcePath) }
450
+ : {}),
451
+ ...(options.stripSkillName !== undefined
452
+ ? { stripSkillName: requireBoolean(options.stripSkillName, 'stripSkillName', node, state, sourcePath) }
453
+ : {}),
454
+ parameters: options.parameters === undefined
455
+ ? {}
456
+ : requireObject(options.parameters, 'useProfileSkill parameters', node, state, sourcePath),
457
+ output: requireTagged(options.output, 'state', node, state, sourcePath),
458
+ result: requireTagged(options.result, 'text_result', node, state, sourcePath),
459
+ };
460
+ }
317
461
  case 'trigger': {
318
462
  exactArgs(name, args, 2, node, state, sourcePath);
319
463
  const operation = requireTagged(args[0], 'operation', node, state, sourcePath);
@@ -342,10 +486,13 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
342
486
  case 'defineSkillApp': {
343
487
  exactArgs(name, args, 1, node, state, sourcePath);
344
488
  const template = requireObject(args[0], 'defineSkillApp template', node, state, sourcePath);
345
- rejectUnknown(template, ['app', 'ui'], name, node, state, sourcePath);
489
+ rejectUnknown(template, ['app', 'permissions', 'ui'], name, node, state, sourcePath);
346
490
  return {
347
491
  __kind: 'skillapp',
348
492
  app: requireObject(template.app, 'app metadata', node, state, sourcePath),
493
+ permissions: template.permissions === undefined
494
+ ? []
495
+ : requireArray(template.permissions, 'defineSkillApp permissions', node, state, sourcePath),
349
496
  ui: requireTagged(template.ui, 'component', node, state, sourcePath),
350
497
  };
351
498
  }
@@ -366,7 +513,7 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
366
513
  case 'Textarea': {
367
514
  argsRange(name, args, 2, 3, node, state, sourcePath);
368
515
  const options = args[2] === undefined ? {} : requireObject(args[2], 'Textarea options', node, state, sourcePath);
369
- rejectUnknown(options, ['showLabel', 'grow', 'editable', 'copyable', 'placeholder'], name, node, state, sourcePath);
516
+ rejectUnknown(options, ['showLabel', 'grow', 'editable', 'copyable', 'rows', 'autoGrow', 'placeholder'], name, node, state, sourcePath);
370
517
  return {
371
518
  __kind: 'component', component: 'textarea', label: requireNonEmptyString(args[0], 'Textarea label', node, state, sourcePath),
372
519
  bind: requireTagged(args[1], 'state', node, state, sourcePath), options,
@@ -377,18 +524,28 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
377
524
  const options = requireObject(args[2], 'Select options', node, state, sourcePath);
378
525
  rejectUnknown(options, ['options', 'showLabel', 'grow'], name, node, state, sourcePath);
379
526
  const choices = requireArray(options.options, 'Select options.options', node, state, sourcePath)
380
- .map(value => requireNonEmptyString(value, 'Select option', node, state, sourcePath));
527
+ .map(value => requireSelectOption(value, node, state, sourcePath));
381
528
  if (choices.length === 0)
382
529
  throw invalidAt(state.sourceFile, node.pos, 'Select options cannot be empty.', sourcePath);
383
530
  return {
384
531
  __kind: 'component', component: 'select', label: requireNonEmptyString(args[0], 'Select label', node, state, sourcePath),
385
- bind: requireTagged(args[1], 'state', node, state, sourcePath), options: { ...options, options: choices },
532
+ bind: requireTagged(args[1], 'state', node, state, sourcePath),
533
+ options: { ...options, options: choices },
534
+ };
535
+ }
536
+ case 'Attachments': {
537
+ argsRange(name, args, 2, 3, node, state, sourcePath);
538
+ const options = args[2] === undefined ? {} : requireObject(args[2], 'Attachments options', node, state, sourcePath);
539
+ rejectUnknown(options, ['showLabel', 'grow'], name, node, state, sourcePath);
540
+ return {
541
+ __kind: 'component', component: 'attachments', label: requireNonEmptyString(args[0], 'Attachments label', node, state, sourcePath),
542
+ bind: requireTagged(args[1], 'attachment_state', node, state, sourcePath), options,
386
543
  };
387
544
  }
388
545
  case 'Button': {
389
546
  exactArgs(name, args, 2, node, state, sourcePath);
390
547
  const options = requireObject(args[1], 'Button options', node, state, sourcePath);
391
- rejectUnknown(options, ['trigger', 'emphasis'], name, node, state, sourcePath);
548
+ rejectUnknown(options, ['trigger', 'emphasis', 'alignToField'], name, node, state, sourcePath);
392
549
  return {
393
550
  __kind: 'component', component: 'button', label: requireNonEmptyString(args[0], 'Button label', node, state, sourcePath),
394
551
  operation: requireTagged(options.trigger, 'operation', node, state, sourcePath), options,
@@ -410,20 +567,28 @@ function normalizeComponent(value, state, sourcePath, depth = 0) {
410
567
  if (value.label !== undefined)
411
568
  item.label = requireString(value.label, `${component} label`, state.sourceFile, state, sourcePath);
412
569
  if (value.bind !== undefined)
413
- item.bind = requireNamedReference(value.bind, 'state', state.sourceFile, state, sourcePath);
570
+ item.bind = requireNamedReference(value.bind, component === 'attachments' ? 'attachment_state' : 'state', state.sourceFile, state, sourcePath);
414
571
  if (value.operation !== undefined)
415
572
  item.trigger = requireName(requireTagged(value.operation, 'operation', state.sourceFile, state, sourcePath), 'operation', state.sourceFile, state, sourcePath);
416
573
  copyBoolean(options, 'showLabel', item, state, sourcePath);
417
574
  copyBoolean(options, 'grow', item, state, sourcePath);
418
575
  copyBoolean(options, 'editable', item, state, sourcePath);
419
576
  copyBoolean(options, 'copyable', item, state, sourcePath);
577
+ copyBoolean(options, 'autoGrow', item, state, sourcePath);
578
+ copyBoolean(options, 'alignToField', item, state, sourcePath);
579
+ if (options.rows !== undefined) {
580
+ item.rows = requireNonNegativeInteger(options.rows, 'rows', state.sourceFile, state, sourcePath);
581
+ if (item.rows < 1 || item.rows > 40) {
582
+ throw MarifoldError_1.MarifoldError.appInvalid('Textarea rows must be between 1 and 40.', sourcePath);
583
+ }
584
+ }
420
585
  copyString(options, 'placeholder', item, state, sourcePath);
421
586
  copyString(options, 'gap', item, state, sourcePath);
422
587
  copyString(options, 'responsive', item, state, sourcePath);
423
588
  copyString(options, 'emphasis', item, state, sourcePath);
424
589
  if (options.options !== undefined) {
425
590
  item.options = requireArray(options.options, 'Select options', state.sourceFile, state, sourcePath)
426
- .map(choice => requireString(choice, 'Select option', state.sourceFile, state, sourcePath));
591
+ .map(choice => requireSelectOption(choice, state.sourceFile, state, sourcePath));
427
592
  }
428
593
  if (item.component === 'textarea' && item.editable === undefined)
429
594
  item.editable = true;
@@ -438,30 +603,62 @@ function normalizeComponent(value, state, sourcePath, depth = 0) {
438
603
  if (item.emphasis !== undefined && !['primary', 'secondary'].includes(item.emphasis)) {
439
604
  throw MarifoldError_1.MarifoldError.appInvalid(`Invalid button emphasis '${item.emphasis}'.`, sourcePath);
440
605
  }
441
- if (item.options && new Set(item.options).size !== item.options.length) {
606
+ if (item.options && new Set(item.options.map(selectOptionValue)).size !== item.options.length) {
442
607
  throw MarifoldError_1.MarifoldError.appInvalid('Select options must be unique.', sourcePath);
443
608
  }
444
609
  return item;
445
610
  }
446
611
  function validateReferences(state, layout, sourcePath) {
447
612
  const stateNames = new Set(state.states.map(item => item.name));
613
+ const attachmentStateNames = new Set(state.attachmentStates.map(item => item.name));
448
614
  const modelNames = new Set(state.models.map(item => item.name));
615
+ const profileNames = new Set(state.profiles.map(item => item.name));
449
616
  const skillNames = new Set(state.skills.map(item => item.name));
450
617
  const operationNames = new Set(state.operations.map(item => item.name));
451
618
  assertUnique(state.models.map(item => item.name), 'model declarations', sourcePath);
619
+ assertUnique(state.profiles.map(item => item.name), 'profile declarations', sourcePath);
452
620
  assertUnique(state.skills.map(item => item.name), 'registered Skills', sourcePath);
453
621
  assertUnique(state.operations.map(item => item.name), 'operations', sourcePath);
622
+ assertUnique(state.attachmentStates.map(item => item.name), 'attachment state declarations', sourcePath);
454
623
  if (state.states.length === 0)
455
624
  throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must declare at least one State.', sourcePath);
456
625
  if (state.operations.length === 0)
457
- throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must declare at least one useSkill operation.', sourcePath);
626
+ throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must declare at least one useSkill or useProfileSkill operation.', sourcePath);
458
627
  for (const operation of state.operations) {
459
- if (!modelNames.has(operation.model))
460
- throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing model '${operation.model}'.`, sourcePath);
461
- if (!skillNames.has(operation.skill))
462
- throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing skill '${operation.skill}'.`, sourcePath);
628
+ if (operation.profile) {
629
+ if (!profileNames.has(operation.profile))
630
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing profile '${operation.profile}'.`, sourcePath);
631
+ if (operation.model)
632
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' cannot reference both a profile and model.`, sourcePath);
633
+ const fixedSkill = operation.skill !== undefined;
634
+ const selectedSkill = operation.skillState !== undefined;
635
+ if (fixedSkill === selectedSkill) {
636
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' must reference exactly one fixed or state-selected profile Skill.`, sourcePath);
637
+ }
638
+ if (selectedSkill) {
639
+ if (!stateNames.has(operation.skillState))
640
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing Skill state '${operation.skillState}'.`, sourcePath);
641
+ if (!operation.skillOptions?.length)
642
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' requires a non-empty Skill allowlist.`, sourcePath);
643
+ const initial = state.states.find(candidate => candidate.name === operation.skillState)?.initial;
644
+ if (initial !== undefined && !operation.skillOptions.includes(initial)) {
645
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' initial Skill '${initial}' is not allowlisted.`, sourcePath);
646
+ }
647
+ }
648
+ }
649
+ else {
650
+ if (!operation.model || !modelNames.has(operation.model))
651
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing model '${operation.model ?? ''}'.`, sourcePath);
652
+ if (!operation.skill || !skillNames.has(operation.skill))
653
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing skill '${operation.skill ?? ''}'.`, sourcePath);
654
+ }
463
655
  if (!stateNames.has(operation.output))
464
656
  throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing output state '${operation.output}'.`, sourcePath);
657
+ if (operation.input && !stateNames.has(operation.input))
658
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing input state '${operation.input}'.`, sourcePath);
659
+ if (operation.attachments && !attachmentStateNames.has(operation.attachments)) {
660
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing attachment state '${operation.attachments}'.`, sourcePath);
661
+ }
465
662
  for (const name of Object.values(operation.parameters)) {
466
663
  if (!stateNames.has(name))
467
664
  throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing state '${name}'.`, sourcePath);
@@ -470,16 +667,31 @@ function validateReferences(state, layout, sourcePath) {
470
667
  const layoutItems = flatten(layout);
471
668
  if (layoutItems.length > 100)
472
669
  throw MarifoldError_1.MarifoldError.appInvalid('SkillApp layout cannot exceed 100 components.', sourcePath);
670
+ for (const operation of state.operations.filter(candidate => candidate.skillState)) {
671
+ const selectors = layoutItems.filter(item => item.component === 'select' && item.bind === operation.skillState);
672
+ if (selectors.length === 0) {
673
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' Skill state '${operation.skillState}' must bind to Select.`, sourcePath);
674
+ }
675
+ const selectable = new Set(selectors.flatMap(item => (item.options ?? []).map(selectOptionValue)));
676
+ const allowlisted = new Set(operation.skillOptions ?? []);
677
+ if (selectable.size !== allowlisted.size || [...selectable].some(skill => !allowlisted.has(skill))) {
678
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' Select options must exactly match its Skill allowlist.`, sourcePath);
679
+ }
680
+ }
473
681
  for (const item of layoutItems) {
474
682
  if (item.component === 'app')
475
683
  throw MarifoldError_1.MarifoldError.appInvalid('App(...) can only be the root UI component.', sourcePath);
476
- if (item.bind && !stateNames.has(item.bind))
684
+ if (item.bind && item.component === 'attachments' && !attachmentStateNames.has(item.bind)) {
685
+ throw MarifoldError_1.MarifoldError.appInvalid(`Layout references missing attachment state '${item.bind}'.`, sourcePath);
686
+ }
687
+ if (item.bind && item.component !== 'attachments' && !stateNames.has(item.bind)) {
477
688
  throw MarifoldError_1.MarifoldError.appInvalid(`Layout references missing state '${item.bind}'.`, sourcePath);
689
+ }
478
690
  if (item.trigger && !operationNames.has(item.trigger))
479
691
  throw MarifoldError_1.MarifoldError.appInvalid(`Button references missing operation '${item.trigger}'.`, sourcePath);
480
692
  if (item.component === 'select' && item.bind) {
481
693
  const initial = state.states.find(candidate => candidate.name === item.bind)?.initial;
482
- if (initial !== undefined && !(item.options ?? []).includes(initial)) {
694
+ if (initial !== undefined && !(item.options ?? []).map(selectOptionValue).includes(initial)) {
483
695
  throw MarifoldError_1.MarifoldError.appInvalid(`Select state '${item.bind}' initial value must be one of its options.`, sourcePath);
484
696
  }
485
697
  }
@@ -494,9 +706,11 @@ function validateReferences(state, layout, sourcePath) {
494
706
  }
495
707
  const outputStates = new Set(state.operations.map(operation => operation.output));
496
708
  for (const operation of state.operations) {
497
- for (const input of Object.values(operation.parameters)) {
709
+ for (const input of [operation.input, operation.skillState, ...Object.values(operation.parameters)]) {
710
+ if (!input)
711
+ continue;
498
712
  if (outputStates.has(input))
499
- throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' cannot use output state '${input}' as a parameter in v1.`, sourcePath);
713
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' cannot use output state '${input}' as an input.`, sourcePath);
500
714
  }
501
715
  }
502
716
  for (const item of layoutItems) {
@@ -516,6 +730,32 @@ function validateReferences(state, layout, sourcePath) {
516
730
  }
517
731
  }
518
732
  }
733
+ function normalizePermissions(values, state, sourcePath) {
734
+ const permissions = values.map(value => ({
735
+ kind: requireString(value.resource, 'permission resource', state.sourceFile, state, sourcePath),
736
+ path: requireNonEmptyString(value.path, 'permission path', state.sourceFile, state, sourcePath),
737
+ access: requireString(value.access, 'permission access', state.sourceFile, state, sourcePath),
738
+ }));
739
+ const duplicate = permissions.find((permission, index) => permissions.findIndex(candidate => (candidate.kind === permission.kind && candidate.path === permission.path)) !== index);
740
+ if (duplicate)
741
+ throw MarifoldError_1.MarifoldError.appInvalid(`Duplicate ${duplicate.kind} permission '${duplicate.path}'.`, sourcePath);
742
+ return permissions;
743
+ }
744
+ function parseModelId(id, node, state, sourcePath) {
745
+ const separator = id.indexOf('/');
746
+ if (separator <= 0 || separator === id.length - 1) {
747
+ throw invalidAt(state.sourceFile, node.pos, `Model '${id}' must use provider/model format.`, sourcePath);
748
+ }
749
+ const provider = id.slice(0, separator);
750
+ if (!SAFE_PROVIDER_NAME.test(provider)) {
751
+ throw invalidAt(state.sourceFile, node.pos, `Invalid model provider '${provider}'.`, sourcePath);
752
+ }
753
+ const model = id.slice(separator + 1);
754
+ if (model.trim() !== model || model.length === 0) {
755
+ throw invalidAt(state.sourceFile, node.pos, `Invalid model id '${model}'.`, sourcePath);
756
+ }
757
+ return { provider, model };
758
+ }
519
759
  function assertUnique(values, label, sourcePath) {
520
760
  const seen = new Set();
521
761
  for (const value of values) {
@@ -572,6 +812,36 @@ function requireArray(value, label, node, state, sourcePath) {
572
812
  return value;
573
813
  throw invalidAt(state.sourceFile, node.pos, `Expected ${label} to be an array.`, sourcePath);
574
814
  }
815
+ function requireSelectOption(value, node, state, sourcePath) {
816
+ if (typeof value === 'string') {
817
+ return requireNonEmptyString(value, 'Select option', node, state, sourcePath);
818
+ }
819
+ const option = requireObject(value, 'Select option', node, state, sourcePath);
820
+ rejectUnknown(option, ['label', 'value'], 'Select option', node, state, sourcePath);
821
+ return {
822
+ label: requireNonEmptyString(option.label, 'Select option label', node, state, sourcePath),
823
+ value: requireNonEmptyString(option.value, 'Select option value', node, state, sourcePath),
824
+ };
825
+ }
826
+ function requireSkillOptionValues(value, node, state, sourcePath) {
827
+ const skills = requireArray(value, 'useProfileSkill skills', node, state, sourcePath)
828
+ .map(option => selectOptionValue(requireSelectOption(option, node, state, sourcePath)));
829
+ if (skills.length === 0) {
830
+ throw invalidAt(state.sourceFile, node.pos, 'useProfileSkill.skills cannot be empty.', sourcePath);
831
+ }
832
+ for (const skill of skills) {
833
+ if (!SAFE_SKILL_NAME.test(skill)) {
834
+ throw invalidAt(state.sourceFile, node.pos, `Invalid profile skill name '${skill}'.`, sourcePath);
835
+ }
836
+ }
837
+ if (new Set(skills).size !== skills.length) {
838
+ throw invalidAt(state.sourceFile, node.pos, 'useProfileSkill.skills must be unique.', sourcePath);
839
+ }
840
+ return skills;
841
+ }
842
+ function selectOptionValue(option) {
843
+ return typeof option === 'string' ? option : option.value;
844
+ }
575
845
  function requireString(value, label, node, state, sourcePath) {
576
846
  if (typeof value === 'string')
577
847
  return value;