@softize/opus 18.1.0 → 18.2.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 (102) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/PROMOTED.md +4 -5
  3. package/README.md +5 -4
  4. package/bin/cli.mjs +4 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +3 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +6 -2
  7. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +6 -1
  8. package/docs/adr/0014-structural-headers-do-not-carry-description.md +2 -2
  9. package/docs/adr/0015-action-size-follows-interaction-density.md +4 -3
  10. package/docs/adr/{0012-modal-header-only-names-the-surface.md → 0018-modal-header-only-names-the-surface.md} +4 -1
  11. package/docs/adr/{0016-productive-surfaces-use-compact-density.md → 0019-productive-surfaces-use-compact-density.md} +4 -1
  12. package/docs/code-style.md +2 -2
  13. package/docs/consumer-upgrade-propagation.md +1 -1
  14. package/docs/data-products.md +5 -3
  15. package/docs/protocol.md +6 -6
  16. package/docs/releasing.md +28 -4
  17. package/package.json +1 -1
  18. package/registry/skills/build-opus-ui/references/ui-patterns.md +11 -1
  19. package/src/auth/drivers/jwt.ts +2 -1
  20. package/src/core/presentation.ts +143 -58
  21. package/src/core/runtime.ts +25 -4
  22. package/src/core/types.ts +4 -4
  23. package/src/mcp/index.ts +9 -0
  24. package/src/ui/components/patterns/content-header.tsx +1 -1
  25. package/src/ui/components/patterns/form.tsx +1 -1
  26. package/src/ui/components/patterns/presentation.tsx +199 -60
  27. package/src/ui/components/patterns/sidebar.tsx +1 -1
  28. package/src/ui/components/patterns/split.tsx +5 -2
  29. package/src/ui/components/patterns/surface-assistant.tsx +74 -0
  30. package/src/ui/components/primitives/card.tsx +1 -1
  31. package/src/ui/components/primitives/detail.tsx +7 -7
  32. package/src/ui/components/primitives/radio-group.tsx +1 -1
  33. package/src/ui/components/primitives/select.tsx +1 -1
  34. package/src/ui/docs/content/action-form-dialog.md +11 -4
  35. package/src/ui/docs/content/action-form.md +7 -16
  36. package/src/ui/docs/content/action-list-dialog.md +4 -6
  37. package/src/ui/docs/content/action-list.md +46 -4
  38. package/src/ui/docs/content/action-trigger.md +9 -5
  39. package/src/ui/docs/content/action-view.md +12 -8
  40. package/src/ui/docs/content/actions.md +36 -13
  41. package/src/ui/docs/content/ai.md +26 -7
  42. package/src/ui/docs/content/alert.md +4 -3
  43. package/src/ui/docs/content/aspect-ratio.md +2 -2
  44. package/src/ui/docs/content/auth.md +25 -10
  45. package/src/ui/docs/content/avatar.md +1 -1
  46. package/src/ui/docs/content/badge.md +2 -2
  47. package/src/ui/docs/content/breadcrumb.md +3 -2
  48. package/src/ui/docs/content/button.md +31 -7
  49. package/src/ui/docs/content/calendar.md +1 -1
  50. package/src/ui/docs/content/card.md +1 -1
  51. package/src/ui/docs/content/carousel.md +14 -3
  52. package/src/ui/docs/content/chat.md +1 -1
  53. package/src/ui/docs/content/cli.md +13 -7
  54. package/src/ui/docs/content/command.md +34 -2
  55. package/src/ui/docs/content/composer.md +1 -1
  56. package/src/ui/docs/content/content.md +5 -4
  57. package/src/ui/docs/content/customization.md +1 -1
  58. package/src/ui/docs/content/cycle.md +7 -5
  59. package/src/ui/docs/content/data-state.md +6 -5
  60. package/src/ui/docs/content/data.md +3 -3
  61. package/src/ui/docs/content/detail.md +3 -2
  62. package/src/ui/docs/content/dialog.md +2 -2
  63. package/src/ui/docs/content/dictionary-value.md +1 -1
  64. package/src/ui/docs/content/dock.md +23 -2
  65. package/src/ui/docs/content/dot.md +0 -2
  66. package/src/ui/docs/content/drawer.md +1 -1
  67. package/src/ui/docs/content/empty.md +1 -4
  68. package/src/ui/docs/content/events.md +1 -1
  69. package/src/ui/docs/content/field.md +20 -11
  70. package/src/ui/docs/content/getting-started.md +4 -2
  71. package/src/ui/docs/content/icon-picker.md +2 -2
  72. package/src/ui/docs/content/input-otp.md +2 -0
  73. package/src/ui/docs/content/input.md +2 -3
  74. package/src/ui/docs/content/item.md +5 -4
  75. package/src/ui/docs/content/kbd.md +2 -1
  76. package/src/ui/docs/content/mcp.md +10 -4
  77. package/src/ui/docs/content/menu.md +27 -0
  78. package/src/ui/docs/content/page.md +19 -5
  79. package/src/ui/docs/content/pagination.md +9 -2
  80. package/src/ui/docs/content/popover.md +2 -2
  81. package/src/ui/docs/content/presentation.md +85 -58
  82. package/src/ui/docs/content/progress.md +2 -6
  83. package/src/ui/docs/content/runtime.md +8 -5
  84. package/src/ui/docs/content/scheduler.md +1 -1
  85. package/src/ui/docs/content/select.md +13 -8
  86. package/src/ui/docs/content/sidebar.md +3 -2
  87. package/src/ui/docs/content/skeleton.md +1 -1
  88. package/src/ui/docs/content/slider.md +4 -4
  89. package/src/ui/docs/content/spinner.md +3 -3
  90. package/src/ui/docs/content/split.md +80 -19
  91. package/src/ui/docs/content/tabs.md +6 -6
  92. package/src/ui/docs/content/testing.md +4 -2
  93. package/src/ui/docs/content/toast.md +3 -5
  94. package/src/ui/docs/content/toggle.md +37 -0
  95. package/src/ui/docs/content/tooltip.md +4 -3
  96. package/src/ui/docs/content/truncate.md +3 -2
  97. package/src/ui/docs/content/ui.md +3 -1
  98. package/src/ui/docs/content/upgrading.md +43 -13
  99. package/src/ui/docs/doc-client.tsx +1 -1
  100. package/src/ui/docs/registry.tsx +30 -5
  101. package/src/ui/meta.ts +5 -5
  102. package/src/ui/react.tsx +5 -0
@@ -90,7 +90,7 @@ const presentationFieldSchema = z
90
90
  })
91
91
  .strict();
92
92
 
93
- const presentationBodySchema = z
93
+ const presentationActionBodySchema = z
94
94
  .object({
95
95
  action: z.string().min(1),
96
96
  input: z.record(presentationBindingSchema).default({}),
@@ -108,6 +108,27 @@ const presentationBodySchema = z
108
108
  })
109
109
  .strict();
110
110
 
111
+ const presentationComponentBodySchema = z
112
+ .object({
113
+ /** Boundary estático do conteúdo React fornecido pelo consumidor. */
114
+ component: identifierSchema,
115
+ /** Define se o heading principal nasce na casca ou no conteúdo React. */
116
+ heading: z.enum(["shell", "content"]).default("shell"),
117
+ })
118
+ .strict();
119
+
120
+ const presentationBodySchema = z.union([
121
+ presentationActionBodySchema,
122
+ presentationComponentBodySchema,
123
+ ]);
124
+
125
+ const presentationAssistantSchema = z
126
+ .object({
127
+ /** Nome acessível do botão que abre a assistência contextual. */
128
+ triggerLabel: z.string().min(1),
129
+ })
130
+ .strict();
131
+
111
132
  const presentationRouteRecordSchema = z
112
133
  .object({
113
134
  presentation: identifierSchema,
@@ -136,6 +157,7 @@ export const presentationSchema = z
136
157
  title: z.string().min(1),
137
158
  route: presentationRouteSchema.optional(),
138
159
  body: presentationBodySchema,
160
+ assistant: presentationAssistantSchema.optional(),
139
161
  actions: z.array(presentationCommandSchema).default([]),
140
162
  })
141
163
  .strict();
@@ -143,6 +165,12 @@ export const presentationSchema = z
143
165
  export type PresentationDefinition = z.infer<typeof presentationSchema>;
144
166
  export type PresentationDefinitionInput = z.input<typeof presentationSchema>;
145
167
  export type PresentationCommand = PresentationDefinition["actions"][number];
168
+ export type PresentationActionBody = z.infer<
169
+ typeof presentationActionBodySchema
170
+ >;
171
+ export type PresentationComponentBody = z.infer<
172
+ typeof presentationComponentBodySchema
173
+ >;
146
174
  type DefaultedProperty<
147
175
  Input,
148
176
  Key extends PropertyKey,
@@ -174,27 +202,48 @@ type DefinedPresentationEffects<
174
202
  }
175
203
  : PresentationCommand["onSuccess"]
176
204
  : PresentationCommand["onSuccess"];
177
- type PresentationOpen = NonNullable<PresentationDefinition["body"]["open"]>;
205
+ type PresentationOpen = NonNullable<PresentationActionBody["open"]>;
178
206
  type DefinedPresentationOpen<Open> = Open extends object
179
207
  ? Omit<Open, "input"> &
180
208
  Omit<PresentationOpen, "input"> & {
181
209
  input: DefaultedProperty<Open, "input", PresentationOpen["input"]>;
182
210
  }
183
211
  : never;
212
+ type DefinedPresentationActionBody<Body> =
213
+ Body extends z.input<typeof presentationActionBodySchema>
214
+ ? Omit<Body, "input" | "open" | "onSuccess"> &
215
+ Omit<PresentationActionBody, "input" | "open" | "onSuccess"> & {
216
+ input: DefaultedProperty<
217
+ Body,
218
+ "input",
219
+ PresentationActionBody["input"]
220
+ >;
221
+ onSuccess: DefinedPresentationEffects<Body, "onSuccess">;
222
+ } & ("open" extends keyof Body
223
+ ? undefined extends Body["open"]
224
+ ? {
225
+ open?: DefinedPresentationOpen<
226
+ Exclude<Body["open"], undefined>
227
+ >;
228
+ }
229
+ : { open: DefinedPresentationOpen<Body["open"]> }
230
+ : { open?: PresentationActionBody["open"] })
231
+ : never;
232
+ type DefinedPresentationComponentBody<Body> =
233
+ Body extends z.input<typeof presentationComponentBodySchema>
234
+ ? Omit<Body, "heading"> &
235
+ Omit<PresentationComponentBody, "heading"> & {
236
+ heading: DefaultedProperty<
237
+ Body,
238
+ "heading",
239
+ PresentationComponentBody["heading"]
240
+ >;
241
+ }
242
+ : never;
184
243
  type DefinedPresentationBody<Body extends PresentationDefinitionInput["body"]> =
185
- Omit<Body, "input" | "open" | "onSuccess"> &
186
- Omit<PresentationDefinition["body"], "input" | "open" | "onSuccess"> & {
187
- input: DefaultedProperty<
188
- Body,
189
- "input",
190
- PresentationDefinition["body"]["input"]
191
- >;
192
- onSuccess: DefinedPresentationEffects<Body, "onSuccess">;
193
- } & ("open" extends keyof Body
194
- ? undefined extends Body["open"]
195
- ? { open?: DefinedPresentationOpen<Exclude<Body["open"], undefined>> }
196
- : { open: DefinedPresentationOpen<Body["open"]> }
197
- : { open?: PresentationDefinition["body"]["open"] });
244
+ Body extends { component: string }
245
+ ? DefinedPresentationComponentBody<Body>
246
+ : DefinedPresentationActionBody<Body>;
198
247
  type DefinedPresentationCommand<Command> = Command extends object
199
248
  ? Omit<Command, "input" | "blocking" | "onSuccess"> &
200
249
  Omit<PresentationCommand, "input" | "blocking" | "onSuccess"> & {
@@ -272,6 +321,18 @@ export interface PresentationActionRegistry {
272
321
  readonly [name: string]: ActionContract;
273
322
  }
274
323
 
324
+ export function isPresentationActionBody(
325
+ body: PresentationDefinition["body"],
326
+ ): body is PresentationActionBody {
327
+ return "action" in body;
328
+ }
329
+
330
+ export function isPresentationComponentBody(
331
+ body: PresentationDefinition["body"],
332
+ ): body is PresentationComponentBody {
333
+ return "component" in body;
334
+ }
335
+
275
336
  export interface PresentationRouteMatch {
276
337
  definition: PresentationDefinition;
277
338
  input: Record<string, string>;
@@ -312,8 +373,7 @@ export function matchPresentationRoute(
312
373
  const decoded = decodeRouteSegment(received);
313
374
  if (decoded === null) return [];
314
375
  input[expected.slice(1)] = decoded;
315
- }
316
- else if (expected !== received) return [];
376
+ } else if (expected !== received) return [];
317
377
  }
318
378
  return [{ definition, input }];
319
379
  });
@@ -527,46 +587,53 @@ export function validatePresentations(
527
587
  problems.push(`Presentation “${presentation.id}” duplicada.`);
528
588
  byId.set(presentation.id, presentation);
529
589
 
530
- const bodyAction = actions[presentation.body.action];
531
- if (bodyAction === undefined) {
532
- problems.push(
533
- `A action “${presentation.body.action}” não está disponível.`,
534
- );
535
- } else if (bodyAction.kind === "simple") {
536
- problems.push(
537
- `A action simple “${bodyAction.name}” não pode ocupar o body.`,
538
- );
590
+ if (isPresentationComponentBody(presentation.body)) {
591
+ // O conteúdo interno permanece uma fronteira React opaca para a spec da casca.
539
592
  } else {
540
- validateActionBindings(
541
- presentation.body.input,
542
- bodyAction,
543
- `${presentation.id}.body.input`,
544
- problems,
545
- );
546
- if (
547
- bodyAction.kind === "view" &&
548
- presentation.body.fields === undefined
549
- ) {
593
+ const bodyAction = actions[presentation.body.action];
594
+ if (bodyAction === undefined) {
550
595
  problems.push(
551
- `A Presentation “${presentation.id}” precisa declarar fields para a view.`,
596
+ `A action “${presentation.body.action}” não está disponível.`,
552
597
  );
553
- }
554
- if (
555
- bodyAction.kind !== "view" &&
556
- presentation.body.fields !== undefined
557
- ) {
558
- problems.push(`Fields só podem ser declarados para uma action view.`);
559
- }
560
- if (bodyAction.kind !== "list" && presentation.body.open !== undefined) {
561
- problems.push(`Open só pode ser declarado para uma action list.`);
562
- }
563
- if (
564
- bodyAction.kind !== "form" &&
565
- presentation.body.submitLabel !== undefined
566
- ) {
598
+ } else if (bodyAction.kind === "simple") {
567
599
  problems.push(
568
- `SubmitLabel pode ser declarado para uma action form.`,
600
+ `A action simple “${bodyAction.name}” não pode ocupar o body.`,
601
+ );
602
+ } else {
603
+ validateActionBindings(
604
+ presentation.body.input,
605
+ bodyAction,
606
+ `${presentation.id}.body.input`,
607
+ problems,
569
608
  );
609
+ if (
610
+ bodyAction.kind === "view" &&
611
+ presentation.body.fields === undefined
612
+ ) {
613
+ problems.push(
614
+ `A Presentation “${presentation.id}” precisa declarar fields para a view.`,
615
+ );
616
+ }
617
+ if (
618
+ bodyAction.kind !== "view" &&
619
+ presentation.body.fields !== undefined
620
+ ) {
621
+ problems.push(`Fields só podem ser declarados para uma action view.`);
622
+ }
623
+ if (
624
+ bodyAction.kind !== "list" &&
625
+ presentation.body.open !== undefined
626
+ ) {
627
+ problems.push(`Open só pode ser declarado para uma action list.`);
628
+ }
629
+ if (
630
+ bodyAction.kind !== "form" &&
631
+ presentation.body.submitLabel !== undefined
632
+ ) {
633
+ problems.push(
634
+ `SubmitLabel só pode ser declarado para uma action form.`,
635
+ );
636
+ }
570
637
  }
571
638
  }
572
639
 
@@ -615,7 +682,10 @@ export function validatePresentations(
615
682
  problems.push(
616
683
  `A route de “${presentation.id}” consulta a Presentation inexistente “${record.presentation}”.`,
617
684
  );
618
- } else if (actions[source.body.action]?.kind !== "list") {
685
+ } else if (
686
+ !isPresentationActionBody(source.body) ||
687
+ actions[source.body.action]?.kind !== "list"
688
+ ) {
619
689
  problems.push(
620
690
  `A route de “${presentation.id}” exige que “${record.presentation}” tenha uma list action no body.`,
621
691
  );
@@ -633,7 +703,9 @@ export function validatePresentations(
633
703
  }
634
704
  }
635
705
  }
636
- const open = presentation.body.open;
706
+ const open = isPresentationActionBody(presentation.body)
707
+ ? presentation.body.open
708
+ : undefined;
637
709
  if (open !== undefined && !byId.has(open.presentation)) {
638
710
  problems.push(
639
711
  `A lista “${presentation.id}” abre a Presentation inexistente “${open.presentation}”.`,
@@ -650,7 +722,9 @@ export function validatePresentations(
650
722
  );
651
723
  }
652
724
  const action =
653
- target === undefined ? undefined : actions[target.body.action];
725
+ target !== undefined && isPresentationActionBody(target.body)
726
+ ? actions[target.body.action]
727
+ : undefined;
654
728
  if (action !== undefined) {
655
729
  validateActionBindings(
656
730
  open.input,
@@ -660,7 +734,10 @@ export function validatePresentations(
660
734
  );
661
735
  }
662
736
  }
663
- for (const effect of presentation.body.onSuccess) {
737
+ const bodyEffects = isPresentationActionBody(presentation.body)
738
+ ? presentation.body.onSuccess
739
+ : [];
740
+ for (const effect of bodyEffects) {
664
741
  if (effect.effect !== "navigate") continue;
665
742
  const target = byId.get(effect.presentation);
666
743
  if (target === undefined) {
@@ -678,7 +755,9 @@ export function validatePresentations(
678
755
  `O body de “${presentation.id}” navega para “${target.id}” como ${effect.surface}, mas sua route declara ${target.route.surface}.`,
679
756
  );
680
757
  }
681
- const action = actions[target.body.action];
758
+ const action = isPresentationActionBody(target.body)
759
+ ? actions[target.body.action]
760
+ : undefined;
682
761
  if (action !== undefined) {
683
762
  validateActionBindings(
684
763
  effect.input,
@@ -706,7 +785,9 @@ export function validatePresentations(
706
785
  `A action “${command.action}” navega para “${target.id}” como ${effect.surface}, mas sua route declara ${target.route.surface}.`,
707
786
  );
708
787
  }
709
- const action = actions[target.body.action];
788
+ const action = isPresentationActionBody(target.body)
789
+ ? actions[target.body.action]
790
+ : undefined;
710
791
  if (action !== undefined) {
711
792
  validateActionBindings(
712
793
  effect.input,
@@ -724,6 +805,10 @@ export function validatePresentations(
724
805
  problems.push(
725
806
  `A action “${command.action}” abre a Presentation inexistente “${command.target.presentation}”.`,
726
807
  );
808
+ } else if (!isPresentationActionBody(target.body)) {
809
+ problems.push(
810
+ `A Presentation “${target.id}” usa conteúdo de componente e não pode ser aberta pela action “${command.action}”.`,
811
+ );
727
812
  } else if (target.body.action !== command.action) {
728
813
  problems.push(
729
814
  `A Presentation “${target.id}” usa “${target.body.action}”, mas foi aberta por “${command.action}”.`,
@@ -75,6 +75,23 @@ import type {
75
75
 
76
76
  /* eslint-disable @typescript-eslint/no-explicit-any */
77
77
 
78
+ /**
79
+ * Erro devolvido a um cliente de IA que chama uma tool fora do conjunto exposto.
80
+ *
81
+ * Quem executa tools (o agente co-locado e o servidor MCP) só pode rodar o que `aiTools()`
82
+ * anunciou. O modelo, ou um prompt injetado, pode emitir qualquer nome; sem este corte, uma
83
+ * action sem `ai.enabled` — inclusive destrutiva — rodaria dentro da autorização do contexto.
84
+ * A resposta é a mesma para nome inexistente e para action registrada mas não exposta, para
85
+ * que o cliente não descubra quais actions existem.
86
+ */
87
+ export function aiToolNotFound(name: string): ReturnType<typeof error> {
88
+ return error({
89
+ code: 'runtime.action_not_found',
90
+ category: 'not_found',
91
+ message: `Tool "${name}" não está disponível.`,
92
+ })
93
+ }
94
+
78
95
  /** Normaliza o `ai` da action (`boolean | AIConfig`) → config, ou null se não exposta à IA. */
79
96
  function normalizeAiConfig(ai: boolean | AIConfig | undefined): AIConfig | null {
80
97
  if (ai === undefined || ai === false) return null
@@ -620,10 +637,10 @@ export class Runtime {
620
637
  // — 1. Validate input ————————————————————————————————————————————————
621
638
  const validatedInput = await this.validate(action.input, input, 'input')
622
639
 
623
- // — 2. Load —————————————————————————————————————————————————————————
624
- const loaded = await this.runLoaders(action, ctx, validatedInput)
625
-
626
- // 3. Authenticate (public-or-user) ——————————————————————————————————
640
+ // — 2. Authenticate (public-or-user) ——————————————————————————————————
641
+ // Antes dos loaders: eles consultam dados, e um chamador anônimo não pode nem disparar essas
642
+ // consultas nem distinguir, pelo erro de um loader, se um registro existe. A autenticação
643
+ // não depende de nada carregado; a autorização, sim, e por isso vem depois.
627
644
  if (action.public !== true && ctx.user === null) {
628
645
  throw error({
629
646
  code: 'auth.unauthenticated',
@@ -632,6 +649,9 @@ export class Runtime {
632
649
  })
633
650
  }
634
651
 
652
+ // — 3. Load —————————————————————————————————————————————————————————
653
+ const loaded = await this.runLoaders(action, ctx, validatedInput)
654
+
635
655
  // — 4. Authorize ——————————————————————————————————————————————————————
636
656
  if (action.authorize !== undefined) {
637
657
  const authorizeFn = this.compileAuthorize(action.authorize)
@@ -848,6 +868,7 @@ export class Runtime {
848
868
  ...opts,
849
869
  tools,
850
870
  execute: async (name, toolInput) => {
871
+ if (!tools.some((tool) => tool.name === name)) return { error: aiToolNotFound(name) }
851
872
  const cfg = normalizeAiConfig(this.actions.get(name)?.ai)
852
873
  if (cfg !== null && (cfg.destructive === true || cfg.requiresConfirmation === true)) {
853
874
  const approved = opts?.confirm ? await opts.confirm({ name, input: toolInput }) : false
package/src/core/types.ts CHANGED
@@ -620,9 +620,9 @@ export interface FilterSpec {
620
620
 
621
621
  /**
622
622
  * Coluna declarativa de uma `ListAction` — a UI (ActionList) deriva a tabela
623
- * daqui; células custom entram POR CIMA na UI (prop `cells`, chave = key). É a base
624
- * do futuro column picker (o usuário escolher colunas): a lista completa vive no
625
- * contrato, `hidden` marca as que nascem fora.
623
+ * daqui; células custom entram POR CIMA na UI (prop `cells`, chave = key). O seletor
624
+ * de colunas parte desta lista: ela vive completa no contrato, e `hidden` marca as que
625
+ * nascem fora.
626
626
  */
627
627
  export interface ListColumnSpec {
628
628
  /** Chave do item (`Out`) que a coluna mostra. */
@@ -1385,7 +1385,7 @@ export interface ScheduleDef {
1385
1385
  cron?: string
1386
1386
  /** Shorthand interval — ex: '1h', '30m', '15s'. */
1387
1387
  every?: string
1388
- /** Timezone IANA — ex: 'America/Sao_Paulo'. Default: UTC. */
1388
+ /** Timezone IANA — ex: 'America/Sao_Paulo'. Sem ela, vale a hora local do processo. */
1389
1389
  timezone?: string
1390
1390
 
1391
1391
  /** Input passado ao runtime.execute(); estático ou dinâmico. */
package/src/mcp/index.ts CHANGED
@@ -14,6 +14,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
14
14
  import { zodToJsonSchema } from 'zod-to-json-schema'
15
15
  import type { ContextBase, Runtime } from '../core/index.ts'
16
16
  import { readPackageVersion } from '../core/package-version.ts'
17
+ import { aiToolNotFound } from '../core/runtime.ts'
17
18
 
18
19
  export interface OpusMcpOptions {
19
20
  name?: string
@@ -55,6 +56,14 @@ export function createOpusMcpServer(runtime: Runtime, opts: OpusMcpOptions = {})
55
56
  }))
56
57
 
57
58
  server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
59
+ // Executa só o que ListTools anunciou. Sem este corte, qualquer action registrada — sem
60
+ // `ai.enabled`, inclusive destrutiva — rodaria dentro da autorização do contexto.
61
+ if (!runtime.aiTools().some((tool) => tool.name === req.params.name)) {
62
+ return {
63
+ content: [{ type: 'text', text: JSON.stringify({ error: aiToolNotFound(req.params.name) }) }],
64
+ isError: true,
65
+ }
66
+ }
58
67
  const base = opts.resolveContext ? await opts.resolveContext(extra) : ANON
59
68
  const result = await runtime.execute(req.params.name, req.params.arguments ?? {}, base)
60
69
  const payload = result.ok ? result.data : { error: result.error }
@@ -50,7 +50,7 @@ interface ContentBaseProps extends Omit<
50
50
 
51
51
  interface ContentShorthandProps extends ContentBaseProps {
52
52
  title: ReactNode;
53
- /** Total de itens ao lado do título o mesmo `count` de Page. */
53
+ /** Total de itens ao lado do título da seção. A página não carrega contador (ADR 0009). */
54
54
  count?: number;
55
55
  description?: ReactNode;
56
56
  actions?: ReactNode;
@@ -702,7 +702,7 @@ export interface ActionFormProps<
702
702
  onSuccess?: (data: TData) => void;
703
703
  submitLabel?: string;
704
704
  cancelLabel?: string;
705
- /** Tratamento visual do cancelamento. Use outline quando ele dividir o footer com a ação principal. */
705
+ /** Tratamento visual do cancelamento. `ghost` por padrão; `outline` quando o footer distribui as ações por igual. */
706
706
  cancelVariant?: Extract<ButtonVariant, "ghost" | "outline">;
707
707
  onCancel?: () => void;
708
708
  /** Bloqueia campos e ações sem desmontar o formulário. */