@marifold/core 0.65.0 → 0.67.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 (61) hide show
  1. package/dist/agent/AgentRunner.d.ts +6 -0
  2. package/dist/agent/AgentRunner.d.ts.map +1 -1
  3. package/dist/agent/AgentRunner.js +21 -6
  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/agent/tools/SkillAppTools.d.ts +90 -0
  14. package/dist/agent/tools/SkillAppTools.d.ts.map +1 -0
  15. package/dist/agent/tools/SkillAppTools.js +402 -0
  16. package/dist/agent/tools/SkillAppTools.js.map +1 -0
  17. package/dist/app/AppStore.d.ts +11 -1
  18. package/dist/app/AppStore.d.ts.map +1 -1
  19. package/dist/app/AppStore.js +110 -15
  20. package/dist/app/AppStore.js.map +1 -1
  21. package/dist/app/SkillAppCompiler.d.ts.map +1 -1
  22. package/dist/app/SkillAppCompiler.js +370 -44
  23. package/dist/app/SkillAppCompiler.js.map +1 -1
  24. package/dist/app/SkillAppDsl.d.ts +77 -4
  25. package/dist/app/SkillAppDsl.d.ts.map +1 -1
  26. package/dist/app/SkillAppDsl.js +53 -0
  27. package/dist/app/SkillAppDsl.js.map +1 -1
  28. package/dist/app/SkillAppInstanceRegistry.d.ts +23 -2
  29. package/dist/app/SkillAppInstanceRegistry.d.ts.map +1 -1
  30. package/dist/app/SkillAppInstanceRegistry.js +439 -11
  31. package/dist/app/SkillAppInstanceRegistry.js.map +1 -1
  32. package/dist/app/SkillAppResolver.d.ts +5 -2
  33. package/dist/app/SkillAppResolver.d.ts.map +1 -1
  34. package/dist/app/SkillAppResolver.js +66 -15
  35. package/dist/app/SkillAppResolver.js.map +1 -1
  36. package/dist/app/SkillAppSchema.d.ts +120 -9
  37. package/dist/app/SkillAppSchema.d.ts.map +1 -1
  38. package/dist/app/SkillAppSchema.js +2 -1
  39. package/dist/app/SkillAppSchema.js.map +1 -1
  40. package/dist/index.d.ts +6 -6
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +14 -4
  43. package/dist/index.js.map +1 -1
  44. package/dist/runtime/MarifoldRuntime.d.ts +14 -8
  45. package/dist/runtime/MarifoldRuntime.d.ts.map +1 -1
  46. package/dist/runtime/MarifoldRuntime.js +246 -30
  47. package/dist/runtime/MarifoldRuntime.js.map +1 -1
  48. package/dist/skill/BuiltInSkillAppBuilder.d.ts +10 -0
  49. package/dist/skill/BuiltInSkillAppBuilder.d.ts.map +1 -0
  50. package/dist/skill/BuiltInSkillAppBuilder.js +34 -0
  51. package/dist/skill/BuiltInSkillAppBuilder.js.map +1 -0
  52. package/dist/skill/BuiltInSkills.d.ts.map +1 -1
  53. package/dist/skill/BuiltInSkills.js +37 -0
  54. package/dist/skill/BuiltInSkills.js.map +1 -1
  55. package/dist/skill/SkillStore.d.ts +2 -2
  56. package/dist/skill/SkillStore.js +2 -2
  57. package/dist/skill/index.d.ts +1 -0
  58. package/dist/skill/index.d.ts.map +1 -1
  59. package/dist/skill/index.js +4 -1
  60. package/dist/skill/index.js.map +1 -1
  61. package/package.json +1 -1
@@ -42,9 +42,14 @@ 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_-]+$/;
46
+ const SAFE_DOWNLOAD_FILENAME = /^[^/\\\u0000-\u001f\u007f]{1,255}$/;
47
+ const SAFE_DOWNLOAD_MEDIA_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*(?:\s*;\s*charset=[a-z0-9._-]+)?$/i;
45
48
  const ALLOWED_BUILDERS = new Set([
46
- 'App', 'Button', 'Column', 'Row', 'Select', 'Spacer', 'State', 'Textarea',
47
- 'TextResult', 'defineSkillApp', 'registerModel', 'registerSkill', 'trigger', 'useSkill',
49
+ 'App', 'AttachmentState', 'Attachments', 'Button', 'Column', 'Download', 'FileAccess',
50
+ 'FolderAccess', 'Markdown', 'Row', 'Select', 'Spacer', 'State', 'Textarea',
51
+ 'TextResult', 'defineSkillApp', 'registerModel', 'registerProfile', 'registerSkill',
52
+ 'trigger', 'useProfileSkill', 'useSkill',
48
53
  ]);
49
54
  /** Compile the restricted TypeScript authoring syntax into renderer-neutral data.
50
55
  * The source is inspected as an AST and is never evaluated or imported. */
@@ -63,7 +68,9 @@ function compileSkillApp(source, sourcePath = 'skillapp.ts') {
63
68
  imports: new Map(),
64
69
  values: new Map(),
65
70
  states: [],
71
+ attachmentStates: [],
66
72
  models: [],
73
+ profiles: [],
67
74
  skills: [],
68
75
  operations: [],
69
76
  triggers: [],
@@ -89,6 +96,8 @@ function compileSkillApp(source, sourcePath = 'skillapp.ts') {
89
96
  const tagged = requireTagged(value, 'skillapp', statement.expression, state, sourcePath);
90
97
  state.template = {
91
98
  app: requireObject(tagged.app, 'defineSkillApp app', statement.expression, state, sourcePath),
99
+ permissions: requireArray(tagged.permissions ?? [], 'defineSkillApp permissions', statement.expression, state, sourcePath)
100
+ .map(permission => requireTagged(permission, 'permission', statement.expression, state, sourcePath)),
92
101
  ui: requireTagged(tagged.ui, 'component', statement.expression, state, sourcePath),
93
102
  };
94
103
  }
@@ -99,16 +108,20 @@ function compileSkillApp(source, sourcePath = 'skillapp.ts') {
99
108
  if (!state.template)
100
109
  throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must export default defineSkillApp({...}).', sourcePath);
101
110
  const app = normalizeAppInfo(state.template.app, state, sourcePath);
111
+ const permissions = normalizePermissions(state.template.permissions, state, sourcePath);
102
112
  const layoutRoot = normalizeComponent(state.template.ui, state, sourcePath);
103
113
  if (layoutRoot.component !== 'app') {
104
114
  throw MarifoldError_1.MarifoldError.appInvalid('defineSkillApp.ui must be App([...]).', sourcePath);
105
115
  }
106
116
  validateReferences(state, layoutRoot.children ?? [], sourcePath);
107
117
  return {
108
- schema: SkillAppSchema_1.SKILL_APP_SCHEMA,
118
+ schema: state.profiles.length > 0 ? SkillAppSchema_1.SKILL_APP_PROFILE_SCHEMA : SkillAppSchema_1.SKILL_APP_SCHEMA,
109
119
  app,
110
120
  states: state.states,
121
+ attachmentStates: state.attachmentStates,
122
+ permissions,
111
123
  models: state.models,
124
+ profiles: state.profiles,
112
125
  skills: state.skills,
113
126
  operations: state.operations,
114
127
  triggers: state.triggers,
@@ -156,20 +169,12 @@ function registerDeclaration(name, value, node, state, sourcePath) {
156
169
  if (value.__kind === 'state') {
157
170
  state.states.push({ name, initial: requireString(value.initial, 'State initial value', node, state, sourcePath) });
158
171
  }
172
+ else if (value.__kind === 'attachment_state') {
173
+ state.attachmentStates.push({ name });
174
+ }
159
175
  else if (value.__kind === 'model') {
160
176
  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
- }
177
+ const { provider, model } = parseModelId(id, node, state, sourcePath);
173
178
  state.models.push({
174
179
  name,
175
180
  provider,
@@ -177,6 +182,28 @@ function registerDeclaration(name, value, node, state, sourcePath) {
177
182
  think: requireBoolean(value.think, 'model think', node, state, sourcePath),
178
183
  });
179
184
  }
185
+ else if (value.__kind === 'profile') {
186
+ const profile = requireString(value.profileName, 'profile name', node, state, sourcePath);
187
+ if (!SAFE_PROFILE_NAME.test(profile)) {
188
+ throw invalidAt(state.sourceFile, node.pos, `Invalid profile name '${profile}'.`, sourcePath);
189
+ }
190
+ const modelId = value.modelId === undefined
191
+ ? undefined
192
+ : requireString(value.modelId, 'profile model override', node, state, sourcePath);
193
+ const modelOverride = modelId === undefined
194
+ ? undefined
195
+ : parseModelId(modelId, node, state, sourcePath);
196
+ state.profiles.push({
197
+ name,
198
+ profile,
199
+ ...(modelOverride ?? {}),
200
+ ...(value.think !== undefined
201
+ ? { think: requireBoolean(value.think, 'profile think', node, state, sourcePath) }
202
+ : {}),
203
+ memory: requireBoolean(value.memory, 'profile memory', node, state, sourcePath),
204
+ history: requireBoolean(value.history, 'profile history', node, state, sourcePath),
205
+ });
206
+ }
180
207
  else if (value.__kind === 'skill') {
181
208
  const skillName = requireString(value.skillName, 'skill name', node, state, sourcePath);
182
209
  if (!SAFE_SKILL_NAME.test(skillName)) {
@@ -189,18 +216,65 @@ function registerDeclaration(name, value, node, state, sourcePath) {
189
216
  });
190
217
  }
191
218
  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
- });
219
+ const parameters = Object.fromEntries(Object.entries(requireObject(value.parameters, 'operation parameters', node, state, sourcePath)).map(([parameter, reference]) => [
220
+ parameter,
221
+ requireNamedReference(reference, 'state', node, state, sourcePath),
222
+ ]));
223
+ if (value.profile !== undefined) {
224
+ const profile = requireTagged(value.profile, 'profile', node, state, sourcePath);
225
+ const result = requireTagged(value.result, 'text_result', node, state, sourcePath);
226
+ const skillName = value.skillName === undefined
227
+ ? undefined
228
+ : requireString(value.skillName, 'profile skill name', node, state, sourcePath);
229
+ const skillState = value.skillState === undefined
230
+ ? undefined
231
+ : requireNamedReference(value.skillState, 'state', node, state, sourcePath);
232
+ const skillOptions = value.skillOptions === undefined
233
+ ? undefined
234
+ : requireSkillOptionValues(value.skillOptions, node, state, sourcePath);
235
+ state.operations.push({
236
+ name,
237
+ profile: requireName(profile, 'profile', node, state, sourcePath),
238
+ ...(skillName ? { skill: skillName } : {}),
239
+ ...(skillState ? { skillState } : {}),
240
+ ...(skillOptions ? { skillOptions } : {}),
241
+ ...(value.stripSkillName !== undefined
242
+ ? { stripSkillName: requireBoolean(value.stripSkillName, 'stripSkillName', node, state, sourcePath) }
243
+ : {}),
244
+ ...(value.input !== undefined
245
+ ? { input: requireNamedReference(value.input, 'state', node, state, sourcePath) }
246
+ : {}),
247
+ ...(value.attachments !== undefined
248
+ ? { attachments: requireNamedReference(value.attachments, 'attachment_state', node, state, sourcePath) }
249
+ : {}),
250
+ parameters,
251
+ requiredInputs: [],
252
+ output: requireNamedReference(value.output, 'state', node, state, sourcePath),
253
+ result: {
254
+ kind: 'text',
255
+ trim: requireBoolean(result.trim, 'result trim', node, state, sourcePath),
256
+ },
257
+ ...(value.interactive !== undefined
258
+ ? { interactive: requireBoolean(value.interactive, 'interactive', node, state, sourcePath) }
259
+ : {}),
260
+ execution: {
261
+ memory: requireBoolean(profile.memory, 'profile memory', node, state, sourcePath),
262
+ history: requireBoolean(profile.history, 'profile history', node, state, sourcePath),
263
+ profileContext: true,
264
+ },
265
+ });
266
+ }
267
+ else {
268
+ state.operations.push({
269
+ name,
270
+ model: requireNamedReference(value.model, 'model', node, state, sourcePath),
271
+ skill: requireNamedReference(value.skill, 'skill', node, state, sourcePath, true),
272
+ parameters,
273
+ requiredInputs: [],
274
+ output: requireNamedReference(value.output, 'state', node, state, sourcePath),
275
+ execution: { memory: false, history: false, profileContext: false },
276
+ });
277
+ }
204
278
  }
205
279
  }
206
280
  function evaluateExpression(rawNode, state, sourcePath, declarationName) {
@@ -269,6 +343,25 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
269
343
  case 'State':
270
344
  exactArgs(name, args, 1, node, state, sourcePath);
271
345
  return { __kind: 'state', initial: requireString(args[0], 'State initial value', node, state, sourcePath) };
346
+ case 'AttachmentState':
347
+ exactArgs(name, args, 0, node, state, sourcePath);
348
+ return { __kind: 'attachment_state' };
349
+ case 'FileAccess':
350
+ case 'FolderAccess': {
351
+ exactArgs(name, args, 2, node, state, sourcePath);
352
+ const options = requireObject(args[1], `${name} options`, node, state, sourcePath);
353
+ rejectUnknown(options, ['access'], name, node, state, sourcePath);
354
+ const access = requireString(options.access, `${name} access`, node, state, sourcePath);
355
+ if (access !== 'read') {
356
+ throw invalidAt(state.sourceFile, node.pos, `${name} access must be "read".`, sourcePath);
357
+ }
358
+ return {
359
+ __kind: 'permission',
360
+ resource: name === 'FileAccess' ? 'file' : 'folder',
361
+ path: requireNonEmptyString(args[0], `${name} path`, node, state, sourcePath),
362
+ access,
363
+ };
364
+ }
272
365
  case 'registerModel': {
273
366
  argsRange(name, args, 1, 2, node, state, sourcePath);
274
367
  const options = args[1] === undefined ? {} : requireObject(args[1], 'registerModel options', node, state, sourcePath);
@@ -279,6 +372,23 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
279
372
  think: options.think === undefined ? false : requireBoolean(options.think, 'think', node, state, sourcePath),
280
373
  };
281
374
  }
375
+ case 'registerProfile': {
376
+ argsRange(name, args, 1, 2, node, state, sourcePath);
377
+ const options = args[1] === undefined ? {} : requireObject(args[1], 'registerProfile options', node, state, sourcePath);
378
+ rejectUnknown(options, ['model', 'think', 'memory', 'history'], name, node, state, sourcePath);
379
+ return {
380
+ __kind: 'profile',
381
+ profileName: requireString(args[0], 'profile name', node, state, sourcePath),
382
+ ...(options.model !== undefined
383
+ ? { modelId: requireString(options.model, 'model', node, state, sourcePath) }
384
+ : {}),
385
+ ...(options.think !== undefined
386
+ ? { think: requireBoolean(options.think, 'think', node, state, sourcePath) }
387
+ : {}),
388
+ memory: options.memory === undefined ? false : requireBoolean(options.memory, 'memory', node, state, sourcePath),
389
+ history: options.history === undefined ? false : requireBoolean(options.history, 'history', node, state, sourcePath),
390
+ };
391
+ }
282
392
  case 'TextResult': {
283
393
  argsRange(name, args, 0, 1, node, state, sourcePath);
284
394
  const options = args[0] === undefined ? {} : requireObject(args[0], 'TextResult options', node, state, sourcePath);
@@ -314,6 +424,52 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
314
424
  output: requireTagged(options.output, 'state', node, state, sourcePath),
315
425
  };
316
426
  }
427
+ case 'useProfileSkill': {
428
+ exactArgs(name, args, 3, node, state, sourcePath);
429
+ const profile = requireTagged(args[0], 'profile', node, state, sourcePath);
430
+ const skillName = typeof args[1] === 'string' ? args[1] : undefined;
431
+ const skillState = skillName === undefined
432
+ ? requireTagged(args[1], 'state', node, state, sourcePath)
433
+ : undefined;
434
+ if (skillName !== undefined && !SAFE_SKILL_NAME.test(skillName)) {
435
+ throw invalidAt(state.sourceFile, node.pos, `Invalid profile skill name '${skillName}'.`, sourcePath);
436
+ }
437
+ const options = requireObject(args[2], 'useProfileSkill options', node, state, sourcePath);
438
+ rejectUnknown(options, ['skills', 'input', 'attachments', 'stripSkillName', 'parameters', 'output', 'result', 'interactive'], name, node, state, sourcePath);
439
+ if (skillName !== undefined && options.skills !== undefined) {
440
+ throw invalidAt(state.sourceFile, node.pos, 'useProfileSkill.skills is only valid when the Skill is selected by State.', sourcePath);
441
+ }
442
+ if (skillState !== undefined && options.skills === undefined) {
443
+ throw invalidAt(state.sourceFile, node.pos, 'A state-selected profile Skill requires a non-empty useProfileSkill.skills allowlist.', sourcePath);
444
+ }
445
+ const interactive = options.interactive === undefined
446
+ ? undefined
447
+ : requireBoolean(options.interactive, 'interactive', node, state, sourcePath);
448
+ if (interactive && skillState !== undefined) {
449
+ throw invalidAt(state.sourceFile, node.pos, 'An interactive profile operation must use one fixed Agent Skill.', sourcePath);
450
+ }
451
+ return {
452
+ __kind: 'operation',
453
+ profile,
454
+ ...(skillName !== undefined ? { skillName } : { skillState }),
455
+ ...(options.skills !== undefined ? { skillOptions: options.skills } : {}),
456
+ ...(options.input !== undefined
457
+ ? { input: requireTagged(options.input, 'state', node, state, sourcePath) }
458
+ : {}),
459
+ ...(options.attachments !== undefined
460
+ ? { attachments: requireTagged(options.attachments, 'attachment_state', node, state, sourcePath) }
461
+ : {}),
462
+ ...(options.stripSkillName !== undefined
463
+ ? { stripSkillName: requireBoolean(options.stripSkillName, 'stripSkillName', node, state, sourcePath) }
464
+ : {}),
465
+ parameters: options.parameters === undefined
466
+ ? {}
467
+ : requireObject(options.parameters, 'useProfileSkill parameters', node, state, sourcePath),
468
+ output: requireTagged(options.output, 'state', node, state, sourcePath),
469
+ result: requireTagged(options.result, 'text_result', node, state, sourcePath),
470
+ ...(interactive !== undefined ? { interactive } : {}),
471
+ };
472
+ }
317
473
  case 'trigger': {
318
474
  exactArgs(name, args, 2, node, state, sourcePath);
319
475
  const operation = requireTagged(args[0], 'operation', node, state, sourcePath);
@@ -342,10 +498,13 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
342
498
  case 'defineSkillApp': {
343
499
  exactArgs(name, args, 1, node, state, sourcePath);
344
500
  const template = requireObject(args[0], 'defineSkillApp template', node, state, sourcePath);
345
- rejectUnknown(template, ['app', 'ui'], name, node, state, sourcePath);
501
+ rejectUnknown(template, ['app', 'permissions', 'ui'], name, node, state, sourcePath);
346
502
  return {
347
503
  __kind: 'skillapp',
348
504
  app: requireObject(template.app, 'app metadata', node, state, sourcePath),
505
+ permissions: template.permissions === undefined
506
+ ? []
507
+ : requireArray(template.permissions, 'defineSkillApp permissions', node, state, sourcePath),
349
508
  ui: requireTagged(template.ui, 'component', node, state, sourcePath),
350
509
  };
351
510
  }
@@ -366,29 +525,73 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
366
525
  case 'Textarea': {
367
526
  argsRange(name, args, 2, 3, node, state, sourcePath);
368
527
  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);
528
+ rejectUnknown(options, ['showLabel', 'grow', 'editable', 'copyable', 'rows', 'autoGrow', 'placeholder'], name, node, state, sourcePath);
370
529
  return {
371
530
  __kind: 'component', component: 'textarea', label: requireNonEmptyString(args[0], 'Textarea label', node, state, sourcePath),
372
531
  bind: requireTagged(args[1], 'state', node, state, sourcePath), options,
373
532
  };
374
533
  }
534
+ case 'Markdown': {
535
+ argsRange(name, args, 2, 3, node, state, sourcePath);
536
+ const options = args[2] === undefined ? {} : requireObject(args[2], 'Markdown options', node, state, sourcePath);
537
+ rejectUnknown(options, ['showLabel', 'grow', 'copyable', 'sourceToggle', 'placeholder'], name, node, state, sourcePath);
538
+ return {
539
+ __kind: 'component', component: 'markdown', label: requireNonEmptyString(args[0], 'Markdown label', node, state, sourcePath),
540
+ bind: requireTagged(args[1], 'state', node, state, sourcePath),
541
+ options: {
542
+ ...options,
543
+ copyable: options.copyable === undefined ? true : requireBoolean(options.copyable, 'copyable', node, state, sourcePath),
544
+ sourceToggle: options.sourceToggle === undefined ? true : requireBoolean(options.sourceToggle, 'sourceToggle', node, state, sourcePath),
545
+ },
546
+ };
547
+ }
548
+ case 'Download': {
549
+ exactArgs(name, args, 3, node, state, sourcePath);
550
+ const options = requireObject(args[2], 'Download options', node, state, sourcePath);
551
+ rejectUnknown(options, ['filename', 'mediaType', 'description', 'showLabel', 'grow'], name, node, state, sourcePath);
552
+ const filename = requireNonEmptyString(options.filename, 'Download filename', node, state, sourcePath);
553
+ if (!SAFE_DOWNLOAD_FILENAME.test(filename) || filename === '.' || filename === '..') {
554
+ throw invalidAt(state.sourceFile, node.pos, 'Download filename must be one safe file name without a path.', sourcePath);
555
+ }
556
+ const mediaType = options.mediaType === undefined
557
+ ? 'text/plain;charset=utf-8'
558
+ : requireNonEmptyString(options.mediaType, 'Download mediaType', node, state, sourcePath);
559
+ if (!SAFE_DOWNLOAD_MEDIA_TYPE.test(mediaType)) {
560
+ throw invalidAt(state.sourceFile, node.pos, `Invalid Download mediaType '${mediaType}'.`, sourcePath);
561
+ }
562
+ return {
563
+ __kind: 'component', component: 'download', label: requireNonEmptyString(args[0], 'Download label', node, state, sourcePath),
564
+ bind: requireTagged(args[1], 'state', node, state, sourcePath),
565
+ options: { ...options, filename, mediaType },
566
+ };
567
+ }
375
568
  case 'Select': {
376
569
  exactArgs(name, args, 3, node, state, sourcePath);
377
570
  const options = requireObject(args[2], 'Select options', node, state, sourcePath);
378
571
  rejectUnknown(options, ['options', 'showLabel', 'grow'], name, node, state, sourcePath);
379
572
  const choices = requireArray(options.options, 'Select options.options', node, state, sourcePath)
380
- .map(value => requireNonEmptyString(value, 'Select option', node, state, sourcePath));
573
+ .map(value => requireSelectOption(value, node, state, sourcePath));
381
574
  if (choices.length === 0)
382
575
  throw invalidAt(state.sourceFile, node.pos, 'Select options cannot be empty.', sourcePath);
383
576
  return {
384
577
  __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 },
578
+ bind: requireTagged(args[1], 'state', node, state, sourcePath),
579
+ options: { ...options, options: choices },
580
+ };
581
+ }
582
+ case 'Attachments': {
583
+ argsRange(name, args, 2, 3, node, state, sourcePath);
584
+ const options = args[2] === undefined ? {} : requireObject(args[2], 'Attachments options', node, state, sourcePath);
585
+ rejectUnknown(options, ['showLabel', 'grow'], name, node, state, sourcePath);
586
+ return {
587
+ __kind: 'component', component: 'attachments', label: requireNonEmptyString(args[0], 'Attachments label', node, state, sourcePath),
588
+ bind: requireTagged(args[1], 'attachment_state', node, state, sourcePath), options,
386
589
  };
387
590
  }
388
591
  case 'Button': {
389
592
  exactArgs(name, args, 2, node, state, sourcePath);
390
593
  const options = requireObject(args[1], 'Button options', node, state, sourcePath);
391
- rejectUnknown(options, ['trigger', 'emphasis'], name, node, state, sourcePath);
594
+ rejectUnknown(options, ['trigger', 'emphasis', 'alignToField'], name, node, state, sourcePath);
392
595
  return {
393
596
  __kind: 'component', component: 'button', label: requireNonEmptyString(args[0], 'Button label', node, state, sourcePath),
394
597
  operation: requireTagged(options.trigger, 'operation', node, state, sourcePath), options,
@@ -410,20 +613,32 @@ function normalizeComponent(value, state, sourcePath, depth = 0) {
410
613
  if (value.label !== undefined)
411
614
  item.label = requireString(value.label, `${component} label`, state.sourceFile, state, sourcePath);
412
615
  if (value.bind !== undefined)
413
- item.bind = requireNamedReference(value.bind, 'state', state.sourceFile, state, sourcePath);
616
+ item.bind = requireNamedReference(value.bind, component === 'attachments' ? 'attachment_state' : 'state', state.sourceFile, state, sourcePath);
414
617
  if (value.operation !== undefined)
415
618
  item.trigger = requireName(requireTagged(value.operation, 'operation', state.sourceFile, state, sourcePath), 'operation', state.sourceFile, state, sourcePath);
416
619
  copyBoolean(options, 'showLabel', item, state, sourcePath);
417
620
  copyBoolean(options, 'grow', item, state, sourcePath);
418
621
  copyBoolean(options, 'editable', item, state, sourcePath);
419
622
  copyBoolean(options, 'copyable', item, state, sourcePath);
623
+ copyBoolean(options, 'autoGrow', item, state, sourcePath);
624
+ copyBoolean(options, 'sourceToggle', item, state, sourcePath);
625
+ copyBoolean(options, 'alignToField', item, state, sourcePath);
626
+ if (options.rows !== undefined) {
627
+ item.rows = requireNonNegativeInteger(options.rows, 'rows', state.sourceFile, state, sourcePath);
628
+ if (item.rows < 1 || item.rows > 40) {
629
+ throw MarifoldError_1.MarifoldError.appInvalid('Textarea rows must be between 1 and 40.', sourcePath);
630
+ }
631
+ }
420
632
  copyString(options, 'placeholder', item, state, sourcePath);
633
+ copyString(options, 'filename', item, state, sourcePath);
634
+ copyString(options, 'mediaType', item, state, sourcePath);
635
+ copyString(options, 'description', item, state, sourcePath);
421
636
  copyString(options, 'gap', item, state, sourcePath);
422
637
  copyString(options, 'responsive', item, state, sourcePath);
423
638
  copyString(options, 'emphasis', item, state, sourcePath);
424
639
  if (options.options !== undefined) {
425
640
  item.options = requireArray(options.options, 'Select options', state.sourceFile, state, sourcePath)
426
- .map(choice => requireString(choice, 'Select option', state.sourceFile, state, sourcePath));
641
+ .map(choice => requireSelectOption(choice, state.sourceFile, state, sourcePath));
427
642
  }
428
643
  if (item.component === 'textarea' && item.editable === undefined)
429
644
  item.editable = true;
@@ -438,30 +653,65 @@ function normalizeComponent(value, state, sourcePath, depth = 0) {
438
653
  if (item.emphasis !== undefined && !['primary', 'secondary'].includes(item.emphasis)) {
439
654
  throw MarifoldError_1.MarifoldError.appInvalid(`Invalid button emphasis '${item.emphasis}'.`, sourcePath);
440
655
  }
441
- if (item.options && new Set(item.options).size !== item.options.length) {
656
+ if (item.options && new Set(item.options.map(selectOptionValue)).size !== item.options.length) {
442
657
  throw MarifoldError_1.MarifoldError.appInvalid('Select options must be unique.', sourcePath);
443
658
  }
444
659
  return item;
445
660
  }
446
661
  function validateReferences(state, layout, sourcePath) {
447
662
  const stateNames = new Set(state.states.map(item => item.name));
663
+ const attachmentStateNames = new Set(state.attachmentStates.map(item => item.name));
448
664
  const modelNames = new Set(state.models.map(item => item.name));
665
+ const profileNames = new Set(state.profiles.map(item => item.name));
449
666
  const skillNames = new Set(state.skills.map(item => item.name));
450
667
  const operationNames = new Set(state.operations.map(item => item.name));
451
668
  assertUnique(state.models.map(item => item.name), 'model declarations', sourcePath);
669
+ assertUnique(state.profiles.map(item => item.name), 'profile declarations', sourcePath);
452
670
  assertUnique(state.skills.map(item => item.name), 'registered Skills', sourcePath);
453
671
  assertUnique(state.operations.map(item => item.name), 'operations', sourcePath);
672
+ assertUnique(state.attachmentStates.map(item => item.name), 'attachment state declarations', sourcePath);
454
673
  if (state.states.length === 0)
455
674
  throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must declare at least one State.', sourcePath);
456
675
  if (state.operations.length === 0)
457
- throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must declare at least one useSkill operation.', sourcePath);
676
+ throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must declare at least one useSkill or useProfileSkill operation.', sourcePath);
458
677
  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);
678
+ if (operation.interactive && (!operation.profile || !operation.skill || operation.skillState)) {
679
+ throw MarifoldError_1.MarifoldError.appInvalid(`Interactive operation '${operation.name}' must reference one fixed profile Agent Skill.`, sourcePath);
680
+ }
681
+ if (operation.profile) {
682
+ if (!profileNames.has(operation.profile))
683
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing profile '${operation.profile}'.`, sourcePath);
684
+ if (operation.model)
685
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' cannot reference both a profile and model.`, sourcePath);
686
+ const fixedSkill = operation.skill !== undefined;
687
+ const selectedSkill = operation.skillState !== undefined;
688
+ if (fixedSkill === selectedSkill) {
689
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' must reference exactly one fixed or state-selected profile Skill.`, sourcePath);
690
+ }
691
+ if (selectedSkill) {
692
+ if (!stateNames.has(operation.skillState))
693
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing Skill state '${operation.skillState}'.`, sourcePath);
694
+ if (!operation.skillOptions?.length)
695
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' requires a non-empty Skill allowlist.`, sourcePath);
696
+ const initial = state.states.find(candidate => candidate.name === operation.skillState)?.initial;
697
+ if (initial !== undefined && !operation.skillOptions.includes(initial)) {
698
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' initial Skill '${initial}' is not allowlisted.`, sourcePath);
699
+ }
700
+ }
701
+ }
702
+ else {
703
+ if (!operation.model || !modelNames.has(operation.model))
704
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing model '${operation.model ?? ''}'.`, sourcePath);
705
+ if (!operation.skill || !skillNames.has(operation.skill))
706
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing skill '${operation.skill ?? ''}'.`, sourcePath);
707
+ }
463
708
  if (!stateNames.has(operation.output))
464
709
  throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing output state '${operation.output}'.`, sourcePath);
710
+ if (operation.input && !stateNames.has(operation.input))
711
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing input state '${operation.input}'.`, sourcePath);
712
+ if (operation.attachments && !attachmentStateNames.has(operation.attachments)) {
713
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing attachment state '${operation.attachments}'.`, sourcePath);
714
+ }
465
715
  for (const name of Object.values(operation.parameters)) {
466
716
  if (!stateNames.has(name))
467
717
  throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing state '${name}'.`, sourcePath);
@@ -470,16 +720,31 @@ function validateReferences(state, layout, sourcePath) {
470
720
  const layoutItems = flatten(layout);
471
721
  if (layoutItems.length > 100)
472
722
  throw MarifoldError_1.MarifoldError.appInvalid('SkillApp layout cannot exceed 100 components.', sourcePath);
723
+ for (const operation of state.operations.filter(candidate => candidate.skillState)) {
724
+ const selectors = layoutItems.filter(item => item.component === 'select' && item.bind === operation.skillState);
725
+ if (selectors.length === 0) {
726
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' Skill state '${operation.skillState}' must bind to Select.`, sourcePath);
727
+ }
728
+ const selectable = new Set(selectors.flatMap(item => (item.options ?? []).map(selectOptionValue)));
729
+ const allowlisted = new Set(operation.skillOptions ?? []);
730
+ if (selectable.size !== allowlisted.size || [...selectable].some(skill => !allowlisted.has(skill))) {
731
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' Select options must exactly match its Skill allowlist.`, sourcePath);
732
+ }
733
+ }
473
734
  for (const item of layoutItems) {
474
735
  if (item.component === 'app')
475
736
  throw MarifoldError_1.MarifoldError.appInvalid('App(...) can only be the root UI component.', sourcePath);
476
- if (item.bind && !stateNames.has(item.bind))
737
+ if (item.bind && item.component === 'attachments' && !attachmentStateNames.has(item.bind)) {
738
+ throw MarifoldError_1.MarifoldError.appInvalid(`Layout references missing attachment state '${item.bind}'.`, sourcePath);
739
+ }
740
+ if (item.bind && item.component !== 'attachments' && !stateNames.has(item.bind)) {
477
741
  throw MarifoldError_1.MarifoldError.appInvalid(`Layout references missing state '${item.bind}'.`, sourcePath);
742
+ }
478
743
  if (item.trigger && !operationNames.has(item.trigger))
479
744
  throw MarifoldError_1.MarifoldError.appInvalid(`Button references missing operation '${item.trigger}'.`, sourcePath);
480
745
  if (item.component === 'select' && item.bind) {
481
746
  const initial = state.states.find(candidate => candidate.name === item.bind)?.initial;
482
- if (initial !== undefined && !(item.options ?? []).includes(initial)) {
747
+ if (initial !== undefined && !(item.options ?? []).map(selectOptionValue).includes(initial)) {
483
748
  throw MarifoldError_1.MarifoldError.appInvalid(`Select state '${item.bind}' initial value must be one of its options.`, sourcePath);
484
749
  }
485
750
  }
@@ -487,6 +752,9 @@ function validateReferences(state, layout, sourcePath) {
487
752
  for (const triggerDefinition of state.triggers) {
488
753
  if (!operationNames.has(triggerDefinition.operation))
489
754
  throw MarifoldError_1.MarifoldError.appInvalid(`Trigger references missing operation '${triggerDefinition.operation}'.`, sourcePath);
755
+ if (state.operations.find(operation => operation.name === triggerDefinition.operation)?.interactive) {
756
+ throw MarifoldError_1.MarifoldError.appInvalid(`Interactive operation '${triggerDefinition.operation}' cannot use an automatic trigger.`, sourcePath);
757
+ }
490
758
  for (const name of triggerDefinition.onChange) {
491
759
  if (!stateNames.has(name))
492
760
  throw MarifoldError_1.MarifoldError.appInvalid(`Trigger references missing state '${name}'.`, sourcePath);
@@ -494,9 +762,11 @@ function validateReferences(state, layout, sourcePath) {
494
762
  }
495
763
  const outputStates = new Set(state.operations.map(operation => operation.output));
496
764
  for (const operation of state.operations) {
497
- for (const input of Object.values(operation.parameters)) {
765
+ for (const input of [operation.input, operation.skillState, ...Object.values(operation.parameters)]) {
766
+ if (!input)
767
+ continue;
498
768
  if (outputStates.has(input))
499
- throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' cannot use output state '${input}' as a parameter in v1.`, sourcePath);
769
+ throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' cannot use output state '${input}' as an input.`, sourcePath);
500
770
  }
501
771
  }
502
772
  for (const item of layoutItems) {
@@ -516,6 +786,32 @@ function validateReferences(state, layout, sourcePath) {
516
786
  }
517
787
  }
518
788
  }
789
+ function normalizePermissions(values, state, sourcePath) {
790
+ const permissions = values.map(value => ({
791
+ kind: requireString(value.resource, 'permission resource', state.sourceFile, state, sourcePath),
792
+ path: requireNonEmptyString(value.path, 'permission path', state.sourceFile, state, sourcePath),
793
+ access: requireString(value.access, 'permission access', state.sourceFile, state, sourcePath),
794
+ }));
795
+ const duplicate = permissions.find((permission, index) => permissions.findIndex(candidate => (candidate.kind === permission.kind && candidate.path === permission.path)) !== index);
796
+ if (duplicate)
797
+ throw MarifoldError_1.MarifoldError.appInvalid(`Duplicate ${duplicate.kind} permission '${duplicate.path}'.`, sourcePath);
798
+ return permissions;
799
+ }
800
+ function parseModelId(id, node, state, sourcePath) {
801
+ const separator = id.indexOf('/');
802
+ if (separator <= 0 || separator === id.length - 1) {
803
+ throw invalidAt(state.sourceFile, node.pos, `Model '${id}' must use provider/model format.`, sourcePath);
804
+ }
805
+ const provider = id.slice(0, separator);
806
+ if (!SAFE_PROVIDER_NAME.test(provider)) {
807
+ throw invalidAt(state.sourceFile, node.pos, `Invalid model provider '${provider}'.`, sourcePath);
808
+ }
809
+ const model = id.slice(separator + 1);
810
+ if (model.trim() !== model || model.length === 0) {
811
+ throw invalidAt(state.sourceFile, node.pos, `Invalid model id '${model}'.`, sourcePath);
812
+ }
813
+ return { provider, model };
814
+ }
519
815
  function assertUnique(values, label, sourcePath) {
520
816
  const seen = new Set();
521
817
  for (const value of values) {
@@ -572,6 +868,36 @@ function requireArray(value, label, node, state, sourcePath) {
572
868
  return value;
573
869
  throw invalidAt(state.sourceFile, node.pos, `Expected ${label} to be an array.`, sourcePath);
574
870
  }
871
+ function requireSelectOption(value, node, state, sourcePath) {
872
+ if (typeof value === 'string') {
873
+ return requireNonEmptyString(value, 'Select option', node, state, sourcePath);
874
+ }
875
+ const option = requireObject(value, 'Select option', node, state, sourcePath);
876
+ rejectUnknown(option, ['label', 'value'], 'Select option', node, state, sourcePath);
877
+ return {
878
+ label: requireNonEmptyString(option.label, 'Select option label', node, state, sourcePath),
879
+ value: requireNonEmptyString(option.value, 'Select option value', node, state, sourcePath),
880
+ };
881
+ }
882
+ function requireSkillOptionValues(value, node, state, sourcePath) {
883
+ const skills = requireArray(value, 'useProfileSkill skills', node, state, sourcePath)
884
+ .map(option => selectOptionValue(requireSelectOption(option, node, state, sourcePath)));
885
+ if (skills.length === 0) {
886
+ throw invalidAt(state.sourceFile, node.pos, 'useProfileSkill.skills cannot be empty.', sourcePath);
887
+ }
888
+ for (const skill of skills) {
889
+ if (!SAFE_SKILL_NAME.test(skill)) {
890
+ throw invalidAt(state.sourceFile, node.pos, `Invalid profile skill name '${skill}'.`, sourcePath);
891
+ }
892
+ }
893
+ if (new Set(skills).size !== skills.length) {
894
+ throw invalidAt(state.sourceFile, node.pos, 'useProfileSkill.skills must be unique.', sourcePath);
895
+ }
896
+ return skills;
897
+ }
898
+ function selectOptionValue(option) {
899
+ return typeof option === 'string' ? option : option.value;
900
+ }
575
901
  function requireString(value, label, node, state, sourcePath) {
576
902
  if (typeof value === 'string')
577
903
  return value;