@nekro-nxt/contracts 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,433 @@
1
+ import { z } from 'zod'
2
+ import { ExtensionCapabilitiesSchema } from './extension-capabilities.js'
3
+ import { ExtensionIdSchema, ExtensionRevisionIdSchema, HostUiPageInstanceIdSchema } from './domain.js'
4
+ import { ConfigSchemaDocumentSchema } from './config-schema.js'
5
+
6
+ export const HostUiPermissionSchema = z.enum([
7
+ 'agents.read',
8
+ 'channels.read',
9
+ 'connections.read',
10
+ 'extensions.read',
11
+ 'dsh-plugins.read',
12
+ 'runtime.read',
13
+ 'messages.read',
14
+ 'assets.read',
15
+ 'agents.manage',
16
+ 'channels.manage',
17
+ 'connections.manage',
18
+ 'credentials.write',
19
+ 'messages.send',
20
+ 'notifications.publish',
21
+ 'network.request',
22
+ ])
23
+
24
+ export type HostUiPermission = z.output<typeof HostUiPermissionSchema>
25
+
26
+ export const HOST_UI_KIT_COMPONENT_NAMES = [
27
+ 'Button',
28
+ 'IconButton',
29
+ 'Input',
30
+ 'Textarea',
31
+ 'Select',
32
+ 'Switch',
33
+ 'Tabs',
34
+ 'Dialog',
35
+ 'Popover',
36
+ 'Tooltip',
37
+ 'Field',
38
+ 'StatusBadge',
39
+ 'InlineFeedback',
40
+ 'EmptyState',
41
+ 'Spinner',
42
+ 'PageHeader',
43
+ 'MetricStrip',
44
+ 'Metric',
45
+ 'Section',
46
+ 'Stack',
47
+ 'Grid',
48
+ 'DataTable',
49
+ 'PropertyList',
50
+ 'PropertyRow',
51
+ ] as const
52
+
53
+ export const HostUiKitComponentNameSchema = z.enum(HOST_UI_KIT_COMPONENT_NAMES)
54
+
55
+ export type HostUiKitComponentName = z.output<typeof HostUiKitComponentNameSchema>
56
+
57
+ export const HostUiPageGeometryEvidenceSchema = z
58
+ .object({
59
+ entryId: z
60
+ .string()
61
+ .trim()
62
+ .regex(/^[a-z](?:[a-z0-9-]{0,62}[a-z0-9])?$/u),
63
+ objectPane: z.enum(['navigation', 'hidden']),
64
+ viewport: z
65
+ .object({
66
+ width: z.number().finite().positive().max(16_384),
67
+ height: z.number().finite().positive().max(16_384),
68
+ })
69
+ .strict(),
70
+ insets: z
71
+ .object({
72
+ top: z.number().finite().nonnegative().max(512),
73
+ right: z.number().finite().nonnegative().max(512),
74
+ bottom: z.number().finite().nonnegative().max(512),
75
+ left: z.number().finite().nonnegative().max(512),
76
+ })
77
+ .strict(),
78
+ contentAxesAligned: z.boolean(),
79
+ horizontalOverflow: z.boolean(),
80
+ titleDistinct: z.boolean(),
81
+ })
82
+ .strict()
83
+
84
+ export type HostUiPageGeometryEvidence = z.output<typeof HostUiPageGeometryEvidenceSchema>
85
+
86
+ export const HostIconNameSchema = z.enum([
87
+ 'app-window',
88
+ 'bar-chart',
89
+ 'book-open',
90
+ 'boxes',
91
+ 'database',
92
+ 'file-text',
93
+ 'folder',
94
+ 'globe',
95
+ 'layout-dashboard',
96
+ 'puzzle',
97
+ 'terminal',
98
+ 'workflow',
99
+ 'wrench',
100
+ ])
101
+
102
+ export type HostIconName = z.output<typeof HostIconNameSchema>
103
+
104
+ export const HostPageIconSchema = z.discriminatedUnion('kind', [
105
+ z.object({ kind: z.literal('host-icon'), name: HostIconNameSchema }).strict(),
106
+ z
107
+ .object({
108
+ kind: z.literal('svg'),
109
+ path: z
110
+ .string()
111
+ .trim()
112
+ .regex(/^assets\/[a-z0-9][a-z0-9/_-]*\.svg$/u),
113
+ sha256: z.string().regex(/^[a-f0-9]{64}$/u),
114
+ })
115
+ .strict(),
116
+ ])
117
+
118
+ export type HostPageIcon = z.output<typeof HostPageIconSchema>
119
+
120
+ export const HostPageContributionSchema = z
121
+ .object({
122
+ kind: z.literal('host-page'),
123
+ entryId: z
124
+ .string()
125
+ .trim()
126
+ .regex(/^[a-z](?:[a-z0-9-]{0,62}[a-z0-9])?$/u),
127
+ title: z.string().trim().min(1).max(40),
128
+ description: z.string().trim().max(120).optional(),
129
+ icon: HostPageIconSchema,
130
+ objectPane: z.enum(['navigation', 'hidden']),
131
+ startPath: z
132
+ .string()
133
+ .trim()
134
+ .regex(/^(?:[a-z0-9][a-z0-9/_-]*)?$/u),
135
+ })
136
+ .strict()
137
+
138
+ export type HostPageContribution = z.output<typeof HostPageContributionSchema>
139
+
140
+ export const HostUiNavigationItemSchema = z
141
+ .object({
142
+ id: z.string().trim().min(1).max(80),
143
+ label: z.string().trim().min(1).max(80),
144
+ description: z.string().trim().max(120).optional(),
145
+ icon: HostPageIconSchema.optional(),
146
+ badge: z.string().trim().max(24).optional(),
147
+ status: z.enum(['neutral', 'info', 'success', 'warning', 'error']).optional(),
148
+ disabledReason: z.string().trim().max(160).optional(),
149
+ path: z
150
+ .string()
151
+ .trim()
152
+ .regex(/^(?:[a-z0-9][a-z0-9/_-]*)?$/u),
153
+ })
154
+ .strict()
155
+
156
+ export const HostUiNavigationGroupSchema = z
157
+ .object({
158
+ id: z.string().trim().min(1).max(80),
159
+ label: z.string().trim().max(80).optional(),
160
+ items: z.array(HostUiNavigationItemSchema).max(256),
161
+ })
162
+ .strict()
163
+
164
+ export const HostUiNavigationModelSchema = z
165
+ .object({
166
+ revision: z.number().int().nonnegative(),
167
+ groups: z.array(HostUiNavigationGroupSchema).max(16),
168
+ })
169
+ .strict()
170
+ .refine((model) => model.groups.reduce((total, group) => total + group.items.length, 0) <= 256, {
171
+ message: 'Host UI navigation cannot contain more than 256 items.',
172
+ })
173
+
174
+ export type HostUiNavigationModel = z.output<typeof HostUiNavigationModelSchema>
175
+
176
+ export const HostUiNetworkOriginSchema = z
177
+ .string()
178
+ .url()
179
+ .transform((value, context) => {
180
+ const url = new URL(value)
181
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
182
+ context.addIssue({ code: 'custom', message: 'Host UI network origins must use HTTP(S).' })
183
+ return z.NEVER
184
+ }
185
+ if (url.pathname !== '/' || url.search || url.hash || url.username || url.password) {
186
+ context.addIssue({ code: 'custom', message: 'Host UI network origins cannot contain paths or credentials.' })
187
+ return z.NEVER
188
+ }
189
+ return url.origin
190
+ })
191
+
192
+ export const HostUiPermissionDeclarationSchema = z
193
+ .object({
194
+ permissions: z.array(HostUiPermissionSchema).max(HostUiPermissionSchema.options.length),
195
+ networkOrigins: z.array(HostUiNetworkOriginSchema).max(32).default([]),
196
+ /** Host capabilities of an agent-scope Revision; absent keeps the pre-capability digest unchanged. */
197
+ capabilities: ExtensionCapabilitiesSchema.optional(),
198
+ })
199
+ .strict()
200
+ .superRefine((value, context) => {
201
+ if (new Set(value.permissions).size !== value.permissions.length) {
202
+ context.addIssue({ code: 'custom', path: ['permissions'], message: 'Host UI permissions must be unique.' })
203
+ }
204
+ if (new Set(value.networkOrigins).size !== value.networkOrigins.length) {
205
+ context.addIssue({ code: 'custom', path: ['networkOrigins'], message: 'Host UI network origins must be unique.' })
206
+ }
207
+ if (value.networkOrigins.length > 0 && !value.permissions.includes('network.request')) {
208
+ context.addIssue({
209
+ code: 'custom',
210
+ path: ['networkOrigins'],
211
+ message: 'Host UI network origins require network.request.',
212
+ })
213
+ }
214
+ })
215
+
216
+ export type HostUiPermissionDeclaration = z.output<typeof HostUiPermissionDeclarationSchema>
217
+
218
+ export const HostUiOwnerSchema = z.discriminatedUnion('kind', [
219
+ z
220
+ .object({
221
+ kind: z.literal('extension'),
222
+ extensionId: ExtensionIdSchema,
223
+ revisionId: ExtensionRevisionIdSchema,
224
+ })
225
+ .strict(),
226
+ z
227
+ .object({
228
+ kind: z.literal('dsh-plugin'),
229
+ entryId: z.string().trim().min(1),
230
+ artifactDigest: z.string().regex(/^[a-f0-9]{64}$/u),
231
+ })
232
+ .strict(),
233
+ ])
234
+
235
+ export type HostUiOwner = z.output<typeof HostUiOwnerSchema>
236
+
237
+ export const HostUiPageEntrySchema = z
238
+ .object({
239
+ pageInstanceId: HostUiPageInstanceIdSchema,
240
+ owner: HostUiOwnerSchema,
241
+ entryId: z.string().trim().min(1).max(64),
242
+ title: z.string().trim().min(1).max(40),
243
+ description: z.string().trim().max(120).optional(),
244
+ icon: HostPageIconSchema,
245
+ objectPane: z.enum(['navigation', 'hidden']),
246
+ startPath: z.string(),
247
+ visible: z.boolean(),
248
+ sortOrder: z.number().int().nonnegative(),
249
+ routeBase: z.string().regex(/^\/apps\/hup_[0-9A-Za-z]+$/u),
250
+ client: z
251
+ .object({
252
+ moduleUrl: z.string().startsWith('/'),
253
+ buildKey: z.string().regex(/^[a-f0-9]{64}$/u),
254
+ })
255
+ .strict(),
256
+ diagnostic: z
257
+ .object({
258
+ status: z.enum(['ready', 'load-failed', 'navigation-failed', 'rpc-failed', 'restore-failed']),
259
+ message: z.string().optional(),
260
+ observedAt: z.number().int().safe().nonnegative(),
261
+ })
262
+ .strict()
263
+ .optional(),
264
+ createdAt: z.number().int().safe().nonnegative(),
265
+ updatedAt: z.number().int().safe().nonnegative(),
266
+ })
267
+ .strict()
268
+
269
+ export type HostUiPageEntry = z.output<typeof HostUiPageEntrySchema>
270
+
271
+ export const HostUiPagePreferencesSchema = z
272
+ .object({
273
+ revision: z.number().int().nonnegative(),
274
+ entries: z.array(
275
+ z
276
+ .object({
277
+ pageInstanceId: HostUiPageInstanceIdSchema,
278
+ visible: z.boolean(),
279
+ })
280
+ .strict(),
281
+ ),
282
+ })
283
+ .strict()
284
+
285
+ export type HostUiPagePreferences = z.output<typeof HostUiPagePreferencesSchema>
286
+
287
+ export const DshNxtHostUiSchema = z
288
+ .object({
289
+ schemaVersion: z.literal(1),
290
+ entryKey: z.string().trim().min(1).max(120),
291
+ client: z
292
+ .string()
293
+ .trim()
294
+ .regex(/^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[a-zA-Z0-9._/-]+\.m?js$/u),
295
+ css: z
296
+ .string()
297
+ .trim()
298
+ .regex(/^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[a-zA-Z0-9._/-]+\.css$/u)
299
+ .optional(),
300
+ pages: z.array(HostPageContributionSchema).min(1).max(8),
301
+ permissions: HostUiPermissionDeclarationSchema.default({ permissions: [], networkOrigins: [] }),
302
+ })
303
+ .strict()
304
+
305
+ export type DshNxtHostUi = z.output<typeof DshNxtHostUiSchema>
306
+
307
+ /** Where the product shell may place an extension panel. The Host decides the concrete page and position. */
308
+ export const ExtensionPanelAnchorSchema = z.enum(['agent', 'channel', 'extension', 'connection'])
309
+ export type ExtensionPanelAnchor = z.output<typeof ExtensionPanelAnchorSchema>
310
+
311
+ export const AGENT_PANEL_ANCHORS = ['agent', 'channel', 'extension'] as const satisfies readonly ExtensionPanelAnchor[]
312
+ export const ADAPTER_PANEL_ANCHORS = ['connection', 'channel'] as const satisfies readonly ExtensionPanelAnchor[]
313
+
314
+ export const PanelDensitySchema = z.enum(['compact', 'full'])
315
+ export type PanelDensity = z.output<typeof PanelDensitySchema>
316
+
317
+ export const ConnectionPanelRoleSchema = z.enum(['setup', 'status', 'diagnostics'])
318
+ export type ConnectionPanelRole = z.output<typeof ConnectionPanelRoleSchema>
319
+
320
+ export const ToolViewDensitySchema = z.enum(['chip', 'card'])
321
+ export type ToolViewDensity = z.output<typeof ToolViewDensitySchema>
322
+
323
+ const ContributionIdSchema = z
324
+ .string()
325
+ .trim()
326
+ .regex(/^[a-z](?:[a-z0-9-]{0,62}[a-z0-9])?$/u)
327
+
328
+ const panelFields = {
329
+ id: ContributionIdSchema,
330
+ anchor: ExtensionPanelAnchorSchema,
331
+ title: z.string().trim().min(1).max(24),
332
+ icon: HostIconNameSchema.optional(),
333
+ densities: z.array(PanelDensitySchema).min(1).max(2),
334
+ role: ConnectionPanelRoleSchema.optional(),
335
+ when: z
336
+ .object({
337
+ channelKinds: z
338
+ .array(z.enum(['internal', 'direct', 'group']))
339
+ .min(1)
340
+ .max(3)
341
+ .optional(),
342
+ })
343
+ .strict()
344
+ .optional(),
345
+ }
346
+
347
+ const checkPanel = (
348
+ value: {
349
+ readonly anchor: ExtensionPanelAnchor
350
+ readonly densities: readonly PanelDensity[]
351
+ readonly role?: ConnectionPanelRole | undefined
352
+ readonly when?: { readonly channelKinds?: readonly string[] | undefined } | undefined
353
+ },
354
+ context: z.RefinementCtx,
355
+ ): void => {
356
+ if (new Set(value.densities).size !== value.densities.length) {
357
+ context.addIssue({ code: 'custom', path: ['densities'], message: '面板密度不能重复。' })
358
+ }
359
+ if (value.anchor === 'connection' && value.role === undefined) {
360
+ context.addIssue({
361
+ code: 'custom',
362
+ path: ['role'],
363
+ message: '连接面板必须声明 setup、status 或 diagnostics 角色。',
364
+ })
365
+ }
366
+ if (value.anchor !== 'connection' && value.role !== undefined) {
367
+ context.addIssue({ code: 'custom', path: ['role'], message: '只有连接面板可以声明角色。' })
368
+ }
369
+ if (value.when?.channelKinds !== undefined && value.anchor !== 'channel') {
370
+ context.addIssue({ code: 'custom', path: ['when'], message: '只有频道面板可以按频道类型限定。' })
371
+ }
372
+ if (
373
+ value.when?.channelKinds !== undefined &&
374
+ new Set(value.when.channelKinds).size !== value.when.channelKinds.length
375
+ ) {
376
+ context.addIssue({ code: 'custom', path: ['when', 'channelKinds'], message: '频道类型不能重复。' })
377
+ }
378
+ }
379
+
380
+ /** Client-side registration options for one panel; identical to the Manifest contribution without `kind`. */
381
+ export const ExtensionPanelDeclarationSchema = z.object(panelFields).strict().superRefine(checkPanel)
382
+ export type ExtensionPanelDeclaration = z.output<typeof ExtensionPanelDeclarationSchema>
383
+
384
+ export const PanelContributionSchema = z
385
+ .object({ kind: z.literal('panel'), ...panelFields })
386
+ .strict()
387
+ .superRefine(checkPanel)
388
+ export type PanelContribution = z.output<typeof PanelContributionSchema>
389
+
390
+ export const ToolViewContributionSchema = z
391
+ .object({ kind: z.literal('tool-view'), tool: z.string().trim().min(1).max(64) })
392
+ .strict()
393
+ export type ToolViewContribution = z.output<typeof ToolViewContributionSchema>
394
+
395
+ export const RichKindSchema = z
396
+ .string()
397
+ .trim()
398
+ .regex(/^[a-z0-9][a-z0-9._-]{0,62}$/u)
399
+
400
+ export const MessageRendererContributionSchema = z
401
+ .object({ kind: z.literal('message-renderer'), richKind: RichKindSchema })
402
+ .strict()
403
+ export type MessageRendererContribution = z.output<typeof MessageRendererContributionSchema>
404
+
405
+ /** Optional configuration surface shared by every extension scope. */
406
+ export const ExtensionConfigDeclarationSchema = z.object({ schema: ConfigSchemaDocumentSchema }).strict()
407
+ export type ExtensionConfigDeclaration = z.output<typeof ExtensionConfigDeclarationSchema>
408
+
409
+ /** Product projection of what one Revision contributes to the shell. */
410
+ export const ExtensionUiContributionsSchema = z
411
+ .object({
412
+ panels: z.array(PanelContributionSchema).max(16),
413
+ toolViews: z.array(z.string().trim().min(1).max(64)).max(32),
414
+ messageRenderers: z.array(RichKindSchema).max(32),
415
+ })
416
+ .strict()
417
+ export type ExtensionUiContributions = z.output<typeof ExtensionUiContributionsSchema>
418
+
419
+ export const EMPTY_EXTENSION_UI_CONTRIBUTIONS: ExtensionUiContributions = {
420
+ panels: [],
421
+ toolViews: [],
422
+ messageRenderers: [],
423
+ }
424
+
425
+ /** Read permission each Client data hook requires; the host enforces it in every runtime that renders a Client. */
426
+ export const EXTENSION_DATA_HOOK_PERMISSIONS = {
427
+ useAgent: 'agents.read',
428
+ useChannel: 'channels.read',
429
+ useConnection: 'connections.read',
430
+ useChannelRuntime: 'runtime.read',
431
+ } as const satisfies Readonly<Record<string, HostUiPermission>>
432
+
433
+ export type ExtensionDataHookName = keyof typeof EXTENSION_DATA_HOOK_PERMISSIONS