@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.
- package/CHANGELOG.md +75 -0
- package/PROMOTED.md +4 -5
- package/README.md +5 -4
- package/bin/cli.mjs +4 -0
- package/docs/adr/0004-page-content-state-is-composed.md +3 -0
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +6 -2
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +6 -1
- package/docs/adr/0014-structural-headers-do-not-carry-description.md +2 -2
- package/docs/adr/0015-action-size-follows-interaction-density.md +4 -3
- package/docs/adr/{0012-modal-header-only-names-the-surface.md → 0018-modal-header-only-names-the-surface.md} +4 -1
- package/docs/adr/{0016-productive-surfaces-use-compact-density.md → 0019-productive-surfaces-use-compact-density.md} +4 -1
- package/docs/code-style.md +2 -2
- package/docs/consumer-upgrade-propagation.md +1 -1
- package/docs/data-products.md +5 -3
- package/docs/protocol.md +6 -6
- package/docs/releasing.md +28 -4
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/references/ui-patterns.md +11 -1
- package/src/auth/drivers/jwt.ts +2 -1
- package/src/core/presentation.ts +143 -58
- package/src/core/runtime.ts +25 -4
- package/src/core/types.ts +4 -4
- package/src/mcp/index.ts +9 -0
- package/src/ui/components/patterns/content-header.tsx +1 -1
- package/src/ui/components/patterns/form.tsx +1 -1
- package/src/ui/components/patterns/presentation.tsx +199 -60
- package/src/ui/components/patterns/sidebar.tsx +1 -1
- package/src/ui/components/patterns/split.tsx +5 -2
- package/src/ui/components/patterns/surface-assistant.tsx +74 -0
- package/src/ui/components/primitives/card.tsx +1 -1
- package/src/ui/components/primitives/detail.tsx +7 -7
- package/src/ui/components/primitives/radio-group.tsx +1 -1
- package/src/ui/components/primitives/select.tsx +1 -1
- package/src/ui/docs/content/action-form-dialog.md +11 -4
- package/src/ui/docs/content/action-form.md +7 -16
- package/src/ui/docs/content/action-list-dialog.md +4 -6
- package/src/ui/docs/content/action-list.md +46 -4
- package/src/ui/docs/content/action-trigger.md +9 -5
- package/src/ui/docs/content/action-view.md +12 -8
- package/src/ui/docs/content/actions.md +36 -13
- package/src/ui/docs/content/ai.md +26 -7
- package/src/ui/docs/content/alert.md +4 -3
- package/src/ui/docs/content/aspect-ratio.md +2 -2
- package/src/ui/docs/content/auth.md +25 -10
- package/src/ui/docs/content/avatar.md +1 -1
- package/src/ui/docs/content/badge.md +2 -2
- package/src/ui/docs/content/breadcrumb.md +3 -2
- package/src/ui/docs/content/button.md +31 -7
- package/src/ui/docs/content/calendar.md +1 -1
- package/src/ui/docs/content/card.md +1 -1
- package/src/ui/docs/content/carousel.md +14 -3
- package/src/ui/docs/content/chat.md +1 -1
- package/src/ui/docs/content/cli.md +13 -7
- package/src/ui/docs/content/command.md +34 -2
- package/src/ui/docs/content/composer.md +1 -1
- package/src/ui/docs/content/content.md +5 -4
- package/src/ui/docs/content/customization.md +1 -1
- package/src/ui/docs/content/cycle.md +7 -5
- package/src/ui/docs/content/data-state.md +6 -5
- package/src/ui/docs/content/data.md +3 -3
- package/src/ui/docs/content/detail.md +3 -2
- package/src/ui/docs/content/dialog.md +2 -2
- package/src/ui/docs/content/dictionary-value.md +1 -1
- package/src/ui/docs/content/dock.md +23 -2
- package/src/ui/docs/content/dot.md +0 -2
- package/src/ui/docs/content/drawer.md +1 -1
- package/src/ui/docs/content/empty.md +1 -4
- package/src/ui/docs/content/events.md +1 -1
- package/src/ui/docs/content/field.md +20 -11
- package/src/ui/docs/content/getting-started.md +4 -2
- package/src/ui/docs/content/icon-picker.md +2 -2
- package/src/ui/docs/content/input-otp.md +2 -0
- package/src/ui/docs/content/input.md +2 -3
- package/src/ui/docs/content/item.md +5 -4
- package/src/ui/docs/content/kbd.md +2 -1
- package/src/ui/docs/content/mcp.md +10 -4
- package/src/ui/docs/content/menu.md +27 -0
- package/src/ui/docs/content/page.md +19 -5
- package/src/ui/docs/content/pagination.md +9 -2
- package/src/ui/docs/content/popover.md +2 -2
- package/src/ui/docs/content/presentation.md +85 -58
- package/src/ui/docs/content/progress.md +2 -6
- package/src/ui/docs/content/runtime.md +8 -5
- package/src/ui/docs/content/scheduler.md +1 -1
- package/src/ui/docs/content/select.md +13 -8
- package/src/ui/docs/content/sidebar.md +3 -2
- package/src/ui/docs/content/skeleton.md +1 -1
- package/src/ui/docs/content/slider.md +4 -4
- package/src/ui/docs/content/spinner.md +3 -3
- package/src/ui/docs/content/split.md +80 -19
- package/src/ui/docs/content/tabs.md +6 -6
- package/src/ui/docs/content/testing.md +4 -2
- package/src/ui/docs/content/toast.md +3 -5
- package/src/ui/docs/content/toggle.md +37 -0
- package/src/ui/docs/content/tooltip.md +4 -3
- package/src/ui/docs/content/truncate.md +3 -2
- package/src/ui/docs/content/ui.md +3 -1
- package/src/ui/docs/content/upgrading.md +43 -13
- package/src/ui/docs/doc-client.tsx +1 -1
- package/src/ui/docs/registry.tsx +30 -5
- package/src/ui/meta.ts +5 -5
- package/src/ui/react.tsx +5 -0
package/src/core/presentation.ts
CHANGED
|
@@ -90,7 +90,7 @@ const presentationFieldSchema = z
|
|
|
90
90
|
})
|
|
91
91
|
.strict();
|
|
92
92
|
|
|
93
|
-
const
|
|
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<
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
531
|
-
|
|
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
|
-
|
|
541
|
-
|
|
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
|
|
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
|
-
`
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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}”.`,
|
package/src/core/runtime.ts
CHANGED
|
@@ -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.
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
//
|
|
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).
|
|
624
|
-
*
|
|
625
|
-
*
|
|
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'.
|
|
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
|
|
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.
|
|
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. */
|