@nekro-nxt/contracts 0.2.0 → 0.4.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/dist/index.mjs +1149 -135
- package/dist/index.mjs.map +1 -1
- package/lib/types/community-personas.d.ts +139 -0
- package/lib/types/community-personas.d.ts.map +1 -0
- package/lib/types/community.d.ts +269 -148
- package/lib/types/community.d.ts.map +1 -1
- package/lib/types/extension-capabilities.d.ts +136 -8
- package/lib/types/extension-capabilities.d.ts.map +1 -1
- package/lib/types/extension-ui.d.ts +275 -5
- package/lib/types/extension-ui.d.ts.map +1 -1
- package/lib/types/host-api.d.ts +5070 -2265
- package/lib/types/host-api.d.ts.map +1 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/community-personas.ts +81 -0
- package/src/community.ts +233 -169
- package/src/extension-capabilities.ts +204 -14
- package/src/extension-ui.ts +142 -15
- package/src/host-api.ts +753 -46
- package/src/index.ts +1 -0
|
@@ -2,10 +2,28 @@ import { z } from 'zod'
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Host capability level this build implements. A Revision that declares `requires.sdk` above it is rejected at
|
|
5
|
-
* import with an "upgrade NekroNXT" message instead of an opaque schema error. Bump
|
|
6
|
-
*
|
|
5
|
+
* import with an "upgrade NekroNXT" message instead of an opaque schema error. Bump when a Manifest capability
|
|
6
|
+
* ships; never reuse a level for a different meaning. Level 7 is Manifest V7 (host instance + agent attachments); level 8 adds the asset library, image info, tool result
|
|
7
|
+
* images, the search index and host-layer model calls.
|
|
7
8
|
*/
|
|
8
|
-
export const EXTENSION_SDK_LEVEL =
|
|
9
|
+
export const EXTENSION_SDK_LEVEL = 8
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The first NekroNXT version of each capability level, so users read "needs NekroNXT 0.5.0" rather than a level
|
|
13
|
+
* number. Add the row for a new level in the change that raises EXTENSION_SDK_LEVEL; a test keeps the two in step.
|
|
14
|
+
*/
|
|
15
|
+
export const EXTENSION_SDK_RELEASES: readonly { readonly level: number; readonly version: string }[] = [
|
|
16
|
+
{ level: 6, version: '0.3.0' },
|
|
17
|
+
{ level: 7, version: '0.4.0' },
|
|
18
|
+
{ level: 8, version: '0.5.0' },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
/** The oldest NekroNXT version that supports `level`; undefined for a level no release supports yet. */
|
|
22
|
+
export const extensionSdkVersion = (level: number): string | undefined =>
|
|
23
|
+
EXTENSION_SDK_RELEASES.find((release) => release.level >= level)?.version
|
|
24
|
+
|
|
25
|
+
/** Manifest V7 itself; extensions that use nothing newer need this level. */
|
|
26
|
+
export const EXTENSION_SDK_BASE_LEVEL = 7
|
|
9
27
|
|
|
10
28
|
export const ExtensionRequiresSchema = z.object({ sdk: z.number().int().min(1).max(1000) }).strict()
|
|
11
29
|
export type ExtensionRequires = z.output<typeof ExtensionRequiresSchema>
|
|
@@ -150,6 +168,86 @@ export const ExtensionPlatformCapabilitySchema = z
|
|
|
150
168
|
.strict()
|
|
151
169
|
export type ExtensionPlatformCapability = z.output<typeof ExtensionPlatformCapabilitySchema>
|
|
152
170
|
|
|
171
|
+
export const EXTENSION_ASSET_LIBRARY_DEFAULT_QUOTA_BYTES = 2 * 1024 * 1024 * 1024
|
|
172
|
+
export const EXTENSION_ASSET_LIBRARY_MAX_QUOTA_BYTES = 32 * 1024 * 1024 * 1024
|
|
173
|
+
|
|
174
|
+
export const ExtensionAssetLibraryCapabilitySchema = z
|
|
175
|
+
.object({
|
|
176
|
+
quotaBytes: z
|
|
177
|
+
.number()
|
|
178
|
+
.int()
|
|
179
|
+
.min(1024 * 1024)
|
|
180
|
+
.max(EXTENSION_ASSET_LIBRARY_MAX_QUOTA_BYTES)
|
|
181
|
+
.optional(),
|
|
182
|
+
})
|
|
183
|
+
.strict()
|
|
184
|
+
export type ExtensionAssetLibraryCapability = z.output<typeof ExtensionAssetLibraryCapabilitySchema>
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* `write` creates Assets in the current channel; `library` keeps Assets in the extension's own library so they can be
|
|
188
|
+
* used in any channel later.
|
|
189
|
+
*/
|
|
190
|
+
export const ExtensionAssetsCapabilitySchema = z
|
|
191
|
+
.object({
|
|
192
|
+
write: z.literal(true).optional(),
|
|
193
|
+
library: ExtensionAssetLibraryCapabilitySchema.optional(),
|
|
194
|
+
})
|
|
195
|
+
.strict()
|
|
196
|
+
.refine((value) => value.write !== undefined || value.library !== undefined, 'assets 至少声明 write 或 library。')
|
|
197
|
+
export type ExtensionAssetsCapability = z.output<typeof ExtensionAssetsCapabilitySchema>
|
|
198
|
+
|
|
199
|
+
const IndexNameSchema = z
|
|
200
|
+
.string()
|
|
201
|
+
.trim()
|
|
202
|
+
.regex(/^[a-z][a-z0-9_]{0,31}$/u, '索引、字段和过滤项名称只能使用小写字母、数字和下划线,以字母开头。')
|
|
203
|
+
|
|
204
|
+
export const EXTENSION_INDEX_MAX_TEXT_FIELDS = 4
|
|
205
|
+
export const EXTENSION_INDEX_MAX_FILTERS = 4
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* One searchable collection. Text fields are matched by keywords (and vectors when the Host has them enabled); weights
|
|
209
|
+
* rank keyword matches. Filter fields hold short keyword values, possibly several per document.
|
|
210
|
+
*/
|
|
211
|
+
export const ExtensionIndexCollectionSchema = z
|
|
212
|
+
.object({
|
|
213
|
+
name: IndexNameSchema,
|
|
214
|
+
fields: z
|
|
215
|
+
.array(z.object({ name: IndexNameSchema, weight: z.number().min(0.1).max(10).default(1) }).strict())
|
|
216
|
+
.min(1)
|
|
217
|
+
.max(EXTENSION_INDEX_MAX_TEXT_FIELDS),
|
|
218
|
+
filters: z.array(IndexNameSchema).max(EXTENSION_INDEX_MAX_FILTERS).default([]),
|
|
219
|
+
})
|
|
220
|
+
.strict()
|
|
221
|
+
.superRefine((value, context) => {
|
|
222
|
+
const names = [...value.fields.map(({ name }) => name), ...value.filters]
|
|
223
|
+
if (new Set(names).size !== names.length) {
|
|
224
|
+
context.addIssue({ code: 'custom', message: '同一索引里的字段与过滤项不能重名。' })
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
export type ExtensionIndexCollection = z.output<typeof ExtensionIndexCollectionSchema>
|
|
228
|
+
|
|
229
|
+
export const ExtensionIndexCapabilitySchema = z
|
|
230
|
+
.object({
|
|
231
|
+
collections: z
|
|
232
|
+
.array(ExtensionIndexCollectionSchema)
|
|
233
|
+
.min(1)
|
|
234
|
+
.max(4)
|
|
235
|
+
.refine((value) => new Set(value.map(({ name }) => name)).size === value.length, '索引名称不能重复。'),
|
|
236
|
+
})
|
|
237
|
+
.strict()
|
|
238
|
+
export type ExtensionIndexCapability = z.output<typeof ExtensionIndexCapabilitySchema>
|
|
239
|
+
|
|
240
|
+
export const EXTENSION_MODEL_MAX_CALLS_PER_MINUTE = 120
|
|
241
|
+
|
|
242
|
+
/** Calls to a model the user picks from the Host's configured models, with images allowed; host layer only. */
|
|
243
|
+
export const ExtensionModelsCapabilitySchema = z
|
|
244
|
+
.object({
|
|
245
|
+
maxCallsPerMinute: z.number().int().min(1).max(EXTENSION_MODEL_MAX_CALLS_PER_MINUTE),
|
|
246
|
+
maxOutputTokens: z.number().int().min(64).max(EXTENSION_LLM_MAX_OUTPUT_TOKENS),
|
|
247
|
+
})
|
|
248
|
+
.strict()
|
|
249
|
+
export type ExtensionModelsCapability = z.output<typeof ExtensionModelsCapabilitySchema>
|
|
250
|
+
|
|
153
251
|
/** A header or environment value: literal text, or the current value of a `meta.role: 'secret'` config field. */
|
|
154
252
|
export const ExtensionMcpValueSchema = z.union([
|
|
155
253
|
z.string().max(4096),
|
|
@@ -252,17 +350,17 @@ const mcpTarget = (server: ExtensionMcpServer): string =>
|
|
|
252
350
|
: JSON.stringify(['streamable-http', server.name, server.url])
|
|
253
351
|
|
|
254
352
|
/**
|
|
255
|
-
*
|
|
256
|
-
* means the
|
|
353
|
+
* Agent-layer capabilities (`permissions.agent`): what the extension may do inside the agents it is enabled for. An
|
|
354
|
+
* absent field means the extension cannot use that capability.
|
|
257
355
|
*/
|
|
258
356
|
export const ExtensionCapabilitiesSchema = z
|
|
259
357
|
.object({
|
|
260
358
|
network: ExtensionNetworkCapabilitySchema.optional(),
|
|
261
359
|
storage: ExtensionStorageCapabilitySchema.optional(),
|
|
262
|
-
assets:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
360
|
+
assets: ExtensionAssetsCapabilitySchema.optional(),
|
|
361
|
+
index: ExtensionIndexCapabilitySchema.optional(),
|
|
362
|
+
/** Host layer only; the agent layer uses `llm`, which runs on the agent's own model. */
|
|
363
|
+
models: ExtensionModelsCapabilitySchema.optional(),
|
|
266
364
|
history: z
|
|
267
365
|
.object({ read: z.literal(true) })
|
|
268
366
|
.strict()
|
|
@@ -281,6 +379,45 @@ export const ExtensionCapabilitiesSchema = z
|
|
|
281
379
|
.strict()
|
|
282
380
|
export type ExtensionCapabilities = z.output<typeof ExtensionCapabilitiesSchema>
|
|
283
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Host-layer capabilities (`permissions.host`): what the extension's single host instance may do through the `nxt`
|
|
384
|
+
* service its factory receives. Host storage is the extension's `shared` partition.
|
|
385
|
+
*/
|
|
386
|
+
export const HostLayerCapabilitiesSchema = z
|
|
387
|
+
.object({
|
|
388
|
+
network: ExtensionNetworkCapabilitySchema.optional(),
|
|
389
|
+
storage: z
|
|
390
|
+
.object({ quotaBytes: z.number().int().min(1024).max(EXTENSION_STORAGE_MAX_QUOTA_BYTES).optional() })
|
|
391
|
+
.strict()
|
|
392
|
+
.optional(),
|
|
393
|
+
/** The same library agent attachments use; the host instance lists and imports into it. */
|
|
394
|
+
assets: z.object({ library: ExtensionAssetLibraryCapabilitySchema }).strict().optional(),
|
|
395
|
+
/** The same collections agent attachments declare; pages search and maintain them. */
|
|
396
|
+
index: ExtensionIndexCapabilitySchema.optional(),
|
|
397
|
+
models: ExtensionModelsCapabilitySchema.optional(),
|
|
398
|
+
})
|
|
399
|
+
.strict()
|
|
400
|
+
export type HostLayerCapabilities = z.output<typeof HostLayerCapabilitiesSchema>
|
|
401
|
+
|
|
402
|
+
/** Host-layer capabilities in the agent-capability shape, so digests, expansion and summaries share one implementation. */
|
|
403
|
+
export const hostLayerAsCapabilities = (host: HostLayerCapabilities | undefined): ExtensionCapabilities | undefined =>
|
|
404
|
+
host === undefined || Object.keys(host).length === 0
|
|
405
|
+
? undefined
|
|
406
|
+
: {
|
|
407
|
+
...(host.network === undefined ? {} : { network: host.network }),
|
|
408
|
+
...(host.storage === undefined
|
|
409
|
+
? {}
|
|
410
|
+
: {
|
|
411
|
+
storage: {
|
|
412
|
+
scopes: ['shared'],
|
|
413
|
+
...(host.storage.quotaBytes === undefined ? {} : { quotaBytes: host.storage.quotaBytes }),
|
|
414
|
+
},
|
|
415
|
+
}),
|
|
416
|
+
...(host.assets === undefined ? {} : { assets: { library: host.assets.library } }),
|
|
417
|
+
...(host.index === undefined ? {} : { index: host.index }),
|
|
418
|
+
...(host.models === undefined ? {} : { models: host.models }),
|
|
419
|
+
}
|
|
420
|
+
|
|
284
421
|
const NETWORK_MODE_RANK = { domains: 0, config: 1, unrestricted: 2 } as const
|
|
285
422
|
|
|
286
423
|
const networkExpands = (
|
|
@@ -313,7 +450,16 @@ export const extensionCapabilitiesExpand = (
|
|
|
313
450
|
if (next === undefined) return false
|
|
314
451
|
if (networkExpands(previous?.network, next.network)) return true
|
|
315
452
|
if (next.storage?.scopes.some((scope) => !(previous?.storage?.scopes ?? []).includes(scope))) return true
|
|
316
|
-
if (next.assets !== undefined && previous?.assets === undefined) return true
|
|
453
|
+
if (next.assets?.write !== undefined && previous?.assets?.write === undefined) return true
|
|
454
|
+
if (next.assets?.library !== undefined && previous?.assets?.library === undefined) return true
|
|
455
|
+
if (
|
|
456
|
+
next.models !== undefined &&
|
|
457
|
+
(previous?.models === undefined ||
|
|
458
|
+
next.models.maxCallsPerMinute > previous.models.maxCallsPerMinute ||
|
|
459
|
+
next.models.maxOutputTokens > previous.models.maxOutputTokens)
|
|
460
|
+
) {
|
|
461
|
+
return true
|
|
462
|
+
}
|
|
317
463
|
if (next.history !== undefined && previous?.history === undefined) return true
|
|
318
464
|
const platformKey = ({ adapter, action }: { readonly adapter: string; readonly action: string }) =>
|
|
319
465
|
`${adapter}:${action}`
|
|
@@ -350,6 +496,11 @@ export const extensionCapabilitiesExpand = (
|
|
|
350
496
|
return false
|
|
351
497
|
}
|
|
352
498
|
|
|
499
|
+
const formatLibraryQuota = (bytes: number): string =>
|
|
500
|
+
bytes >= 1024 * 1024 * 1024
|
|
501
|
+
? `${+(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`
|
|
502
|
+
: `${Math.round(bytes / 1024 / 1024)} MB`
|
|
503
|
+
|
|
353
504
|
/** Capability risk tiers shown on the approval page (§7 of the capability decision). */
|
|
354
505
|
export type ExtensionCapabilityRisk = 'normal' | 'sensitive' | 'high'
|
|
355
506
|
|
|
@@ -360,9 +511,17 @@ export interface ExtensionCapabilitySummaryItem {
|
|
|
360
511
|
readonly detail?: string
|
|
361
512
|
}
|
|
362
513
|
|
|
363
|
-
/**
|
|
514
|
+
/**
|
|
515
|
+
* User-facing summary of declared capabilities, in a stable order, for approval and inspection pages. `layer: 'host'`
|
|
516
|
+
* phrases host-layer capabilities (passed through {@link hostLayerAsCapabilities}) for the host instance.
|
|
517
|
+
*/
|
|
364
518
|
export const summarizeExtensionCapabilities = (
|
|
365
519
|
capabilities: ExtensionCapabilities | undefined,
|
|
520
|
+
options: {
|
|
521
|
+
readonly layer?: 'host' | 'agent'
|
|
522
|
+
/** Display name of a config field, so `network.mode: 'config'` names the fields as the user knows them. */
|
|
523
|
+
readonly fieldTitle?: (key: string) => string
|
|
524
|
+
} = {},
|
|
366
525
|
): readonly ExtensionCapabilitySummaryItem[] => {
|
|
367
526
|
if (capabilities === undefined) return []
|
|
368
527
|
const items: ExtensionCapabilitySummaryItem[] = []
|
|
@@ -370,11 +529,18 @@ export const summarizeExtensionCapabilities = (
|
|
|
370
529
|
if (network?.mode === 'domains') {
|
|
371
530
|
items.push({ key: 'network', risk: 'normal', label: '访问指定网站', detail: network.domains.join('、') })
|
|
372
531
|
} else if (network?.mode === 'config') {
|
|
373
|
-
items.push({
|
|
532
|
+
items.push({
|
|
533
|
+
key: 'network',
|
|
534
|
+
risk: 'normal',
|
|
535
|
+
label: '访问你在配置中填写的地址',
|
|
536
|
+
detail: network.fields.map((field) => options.fieldTitle?.(field) ?? field).join('、'),
|
|
537
|
+
})
|
|
374
538
|
} else if (network?.mode === 'unrestricted') {
|
|
375
539
|
items.push({ key: 'network', risk: 'high', label: '访问任意公网地址', detail: network.purpose })
|
|
376
540
|
}
|
|
377
|
-
if (capabilities.storage !== undefined) {
|
|
541
|
+
if (capabilities.storage !== undefined && options.layer === 'host') {
|
|
542
|
+
items.push({ key: 'storage', risk: 'normal', label: '在本机保存扩展数据,页面与启用它的智能体共用' })
|
|
543
|
+
} else if (capabilities.storage !== undefined) {
|
|
378
544
|
const shared = capabilities.storage.scopes.includes('shared')
|
|
379
545
|
items.push({
|
|
380
546
|
key: 'storage',
|
|
@@ -382,8 +548,32 @@ export const summarizeExtensionCapabilities = (
|
|
|
382
548
|
label: shared ? '保存数据,并在启用它的智能体之间共享' : '为这个智能体保存数据',
|
|
383
549
|
})
|
|
384
550
|
}
|
|
385
|
-
if (capabilities.assets !== undefined)
|
|
551
|
+
if (capabilities.assets?.write !== undefined)
|
|
386
552
|
items.push({ key: 'assets', risk: 'normal', label: '在当前频道生成图片或文件' })
|
|
553
|
+
if (capabilities.assets?.library !== undefined) {
|
|
554
|
+
items.push({
|
|
555
|
+
key: 'assets.library',
|
|
556
|
+
risk: 'sensitive',
|
|
557
|
+
label: '把频道里的图片收进扩展资源库,之后可以在其他频道使用',
|
|
558
|
+
detail: `最多 ${formatLibraryQuota(capabilities.assets.library.quotaBytes ?? EXTENSION_ASSET_LIBRARY_DEFAULT_QUOTA_BYTES)}`,
|
|
559
|
+
})
|
|
560
|
+
}
|
|
561
|
+
if (capabilities.index !== undefined) {
|
|
562
|
+
items.push({
|
|
563
|
+
key: 'index',
|
|
564
|
+
risk: 'normal',
|
|
565
|
+
label: '建立可搜索的索引',
|
|
566
|
+
detail: capabilities.index.collections.map(({ name }) => name).join('、'),
|
|
567
|
+
})
|
|
568
|
+
}
|
|
569
|
+
if (capabilities.models !== undefined) {
|
|
570
|
+
items.push({
|
|
571
|
+
key: 'models',
|
|
572
|
+
risk: 'sensitive',
|
|
573
|
+
label: '使用本机已配置的模型,可以发送图片',
|
|
574
|
+
detail: `每分钟最多 ${capabilities.models.maxCallsPerMinute} 次,计入所选模型的用量`,
|
|
575
|
+
})
|
|
576
|
+
}
|
|
387
577
|
if (capabilities.history !== undefined)
|
|
388
578
|
items.push({ key: 'history', risk: 'normal', label: '读取当前频道的聊天记录' })
|
|
389
579
|
const hook = capabilities.inboundHook
|
package/src/extension-ui.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ExtensionCapabilitiesSchema,
|
|
4
|
+
HostLayerCapabilitiesSchema,
|
|
5
|
+
hostLayerAsCapabilities,
|
|
6
|
+
EXTENSION_SDK_BASE_LEVEL,
|
|
7
|
+
} from './extension-capabilities.js'
|
|
3
8
|
import { ExtensionIdSchema, ExtensionRevisionIdSchema, HostUiPageInstanceIdSchema } from './domain.js'
|
|
4
9
|
import { ConfigSchemaDocumentSchema } from './config-schema.js'
|
|
5
10
|
|
|
@@ -132,11 +137,23 @@ export const HostPageContributionSchema = z
|
|
|
132
137
|
.string()
|
|
133
138
|
.trim()
|
|
134
139
|
.regex(/^(?:[a-z0-9][a-z0-9/_-]*)?$/u),
|
|
140
|
+
/** Registers a navigation rail entry for this page; at most one page of an extension may declare it. */
|
|
141
|
+
rail: z
|
|
142
|
+
.object({ order: z.number().int().min(0).max(999).optional() })
|
|
143
|
+
.strict()
|
|
144
|
+
.optional(),
|
|
135
145
|
})
|
|
136
146
|
.strict()
|
|
137
147
|
|
|
138
148
|
export type HostPageContribution = z.output<typeof HostPageContributionSchema>
|
|
139
149
|
|
|
150
|
+
/** Rail position of a page that registers a rail entry without an explicit order. */
|
|
151
|
+
export const HOST_PAGE_DEFAULT_RAIL_ORDER = 100
|
|
152
|
+
|
|
153
|
+
/** The stored rail position of a page; `null` when the page has no rail entry. */
|
|
154
|
+
export const hostPageRailOrder = (page: Pick<HostPageContribution, 'rail'>): number | null =>
|
|
155
|
+
page.rail === undefined ? null : (page.rail.order ?? HOST_PAGE_DEFAULT_RAIL_ORDER)
|
|
156
|
+
|
|
140
157
|
export const HostUiNavigationItemSchema = z
|
|
141
158
|
.object({
|
|
142
159
|
id: z.string().trim().min(1).max(80),
|
|
@@ -189,31 +206,96 @@ export const HostUiNetworkOriginSchema = z
|
|
|
189
206
|
return url.origin
|
|
190
207
|
})
|
|
191
208
|
|
|
209
|
+
const checkBrowserPermissions = (
|
|
210
|
+
value: { readonly permissions: readonly HostUiPermission[]; readonly networkOrigins: readonly string[] },
|
|
211
|
+
context: z.RefinementCtx,
|
|
212
|
+
): void => {
|
|
213
|
+
if (new Set(value.permissions).size !== value.permissions.length) {
|
|
214
|
+
context.addIssue({ code: 'custom', path: ['permissions'], message: 'Host UI permissions must be unique.' })
|
|
215
|
+
}
|
|
216
|
+
if (new Set(value.networkOrigins).size !== value.networkOrigins.length) {
|
|
217
|
+
context.addIssue({ code: 'custom', path: ['networkOrigins'], message: 'Host UI network origins must be unique.' })
|
|
218
|
+
}
|
|
219
|
+
if (value.networkOrigins.length > 0 && !value.permissions.includes('network.request')) {
|
|
220
|
+
context.addIssue({
|
|
221
|
+
code: 'custom',
|
|
222
|
+
path: ['networkOrigins'],
|
|
223
|
+
message: 'Host UI network origins require network.request.',
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* One approval: browser-side permissions plus the capabilities of one layer. Grants, digests and the approval page
|
|
230
|
+
* use this shape for both the host approval and each agent approval.
|
|
231
|
+
*/
|
|
192
232
|
export const HostUiPermissionDeclarationSchema = z
|
|
193
233
|
.object({
|
|
194
234
|
permissions: z.array(HostUiPermissionSchema).max(HostUiPermissionSchema.options.length),
|
|
195
235
|
networkOrigins: z.array(HostUiNetworkOriginSchema).max(32).default([]),
|
|
196
|
-
/** Host capabilities of an agent-scope Revision; absent keeps the pre-capability digest unchanged. */
|
|
197
236
|
capabilities: ExtensionCapabilitiesSchema.optional(),
|
|
198
237
|
})
|
|
199
238
|
.strict()
|
|
239
|
+
.superRefine(checkBrowserPermissions)
|
|
240
|
+
|
|
241
|
+
export type HostUiPermissionDeclaration = z.output<typeof HostUiPermissionDeclarationSchema>
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Manifest V7 `permissions`: browser-side permissions of pages and panels (`host.call`), host-layer capabilities used
|
|
245
|
+
* by the single host instance, and agent-layer capabilities used inside each enabled agent.
|
|
246
|
+
*/
|
|
247
|
+
export const ExtensionPermissionsSchema = z
|
|
248
|
+
.object({
|
|
249
|
+
permissions: z.array(HostUiPermissionSchema).max(HostUiPermissionSchema.options.length).default([]),
|
|
250
|
+
networkOrigins: z.array(HostUiNetworkOriginSchema).max(32).default([]),
|
|
251
|
+
host: HostLayerCapabilitiesSchema.optional(),
|
|
252
|
+
agent: ExtensionCapabilitiesSchema.optional(),
|
|
253
|
+
})
|
|
254
|
+
.strict()
|
|
255
|
+
.superRefine(checkBrowserPermissions)
|
|
200
256
|
.superRefine((value, context) => {
|
|
201
|
-
if (
|
|
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')) {
|
|
257
|
+
if (value.agent?.models !== undefined) {
|
|
208
258
|
context.addIssue({
|
|
209
259
|
code: 'custom',
|
|
210
|
-
path: ['
|
|
211
|
-
message: '
|
|
260
|
+
path: ['agent', 'models'],
|
|
261
|
+
message: 'models 只能在 permissions.host 中声明;智能体层使用 llm 调用智能体自己的模型。',
|
|
212
262
|
})
|
|
213
263
|
}
|
|
264
|
+
// Both layers read and write the same collections, so a name must mean one definition.
|
|
265
|
+
for (const collection of value.agent?.index?.collections ?? []) {
|
|
266
|
+
const host = value.host?.index?.collections.find(({ name }) => name === collection.name)
|
|
267
|
+
if (host !== undefined && JSON.stringify(host) !== JSON.stringify(collection)) {
|
|
268
|
+
context.addIssue({
|
|
269
|
+
code: 'custom',
|
|
270
|
+
path: ['agent', 'index'],
|
|
271
|
+
message: `索引 ${collection.name} 在 permissions.host 与 permissions.agent 中的定义不一致。`,
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
}
|
|
214
275
|
})
|
|
215
276
|
|
|
216
|
-
export type
|
|
277
|
+
export type ExtensionPermissions = z.output<typeof ExtensionPermissionsSchema>
|
|
278
|
+
|
|
279
|
+
export const EMPTY_EXTENSION_PERMISSIONS: ExtensionPermissions = { permissions: [], networkOrigins: [] }
|
|
280
|
+
|
|
281
|
+
/** What installing the extension on this machine asks the user to approve. */
|
|
282
|
+
export const hostPermissionDeclaration = (permissions: ExtensionPermissions): HostUiPermissionDeclaration => {
|
|
283
|
+
const capabilities = hostLayerAsCapabilities(permissions.host)
|
|
284
|
+
return {
|
|
285
|
+
permissions: [...permissions.permissions],
|
|
286
|
+
networkOrigins: [...permissions.networkOrigins],
|
|
287
|
+
...(capabilities === undefined ? {} : { capabilities }),
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** What enabling the extension for one agent asks the user to approve. */
|
|
292
|
+
export const agentPermissionDeclaration = (permissions: ExtensionPermissions): HostUiPermissionDeclaration => ({
|
|
293
|
+
permissions: [],
|
|
294
|
+
networkOrigins: [],
|
|
295
|
+
...(permissions.agent === undefined || Object.keys(permissions.agent).length === 0
|
|
296
|
+
? {}
|
|
297
|
+
: { capabilities: permissions.agent }),
|
|
298
|
+
})
|
|
217
299
|
|
|
218
300
|
export const HostUiOwnerSchema = z.discriminatedUnion('kind', [
|
|
219
301
|
z
|
|
@@ -246,6 +328,11 @@ export const HostUiPageEntrySchema = z
|
|
|
246
328
|
startPath: z.string(),
|
|
247
329
|
visible: z.boolean(),
|
|
248
330
|
sortOrder: z.number().int().nonnegative(),
|
|
331
|
+
/** Present when the page registered a navigation rail entry; lower orders come first. */
|
|
332
|
+
rail: z
|
|
333
|
+
.object({ order: z.number().int().min(0).max(999) })
|
|
334
|
+
.strict()
|
|
335
|
+
.optional(),
|
|
249
336
|
routeBase: z.string().regex(/^\/apps\/hup_[0-9A-Za-z]+$/u),
|
|
250
337
|
client: z
|
|
251
338
|
.object({
|
|
@@ -308,8 +395,17 @@ export type DshNxtHostUi = z.output<typeof DshNxtHostUiSchema>
|
|
|
308
395
|
export const ExtensionPanelAnchorSchema = z.enum(['agent', 'channel', 'extension', 'connection'])
|
|
309
396
|
export type ExtensionPanelAnchor = z.output<typeof ExtensionPanelAnchorSchema>
|
|
310
397
|
|
|
311
|
-
|
|
312
|
-
|
|
398
|
+
/**
|
|
399
|
+
* Which layer a panel belongs to. Agent and channel panels follow the agents the extension is enabled for (a channel
|
|
400
|
+
* panel also shows on channels of the extension's own adapter); extension and connection panels belong to the host
|
|
401
|
+
* instance.
|
|
402
|
+
*/
|
|
403
|
+
export const PANEL_ANCHOR_LAYER = {
|
|
404
|
+
agent: 'agent',
|
|
405
|
+
channel: 'agent',
|
|
406
|
+
extension: 'host',
|
|
407
|
+
connection: 'host',
|
|
408
|
+
} as const satisfies Readonly<Record<ExtensionPanelAnchor, 'agent' | 'host'>>
|
|
313
409
|
|
|
314
410
|
export const PanelDensitySchema = z.enum(['compact', 'full'])
|
|
315
411
|
export type PanelDensity = z.output<typeof PanelDensitySchema>
|
|
@@ -402,10 +498,22 @@ export const MessageRendererContributionSchema = z
|
|
|
402
498
|
.strict()
|
|
403
499
|
export type MessageRendererContribution = z.output<typeof MessageRendererContributionSchema>
|
|
404
500
|
|
|
405
|
-
/**
|
|
501
|
+
/** What an extension provides, derived from its Manifest; for listing and filtering, never a lifecycle switch. */
|
|
502
|
+
export const EXTENSION_PROVIDES = ['agent', 'page', 'adapter', 'mcp'] as const
|
|
503
|
+
export const ExtensionProvideSchema = z.enum(EXTENSION_PROVIDES)
|
|
504
|
+
export type ExtensionProvide = z.output<typeof ExtensionProvideSchema>
|
|
505
|
+
|
|
506
|
+
/** One configuration form: a serialized Schemastery document. */
|
|
406
507
|
export const ExtensionConfigDeclarationSchema = z.object({ schema: ConfigSchemaDocumentSchema }).strict()
|
|
407
508
|
export type ExtensionConfigDeclaration = z.output<typeof ExtensionConfigDeclarationSchema>
|
|
408
509
|
|
|
510
|
+
/** Manifest V7 `config`: the host instance's configuration and each enabled agent's configuration. */
|
|
511
|
+
export const ExtensionLayeredConfigSchema = z
|
|
512
|
+
.object({ host: ExtensionConfigDeclarationSchema.optional(), agent: ExtensionConfigDeclarationSchema.optional() })
|
|
513
|
+
.strict()
|
|
514
|
+
export type ExtensionLayeredConfig = z.output<typeof ExtensionLayeredConfigSchema>
|
|
515
|
+
export type ExtensionConfigLayer = 'host' | 'agent'
|
|
516
|
+
|
|
409
517
|
/** Product projection of what one Revision contributes to the shell. */
|
|
410
518
|
export const ExtensionUiContributionsSchema = z
|
|
411
519
|
.object({
|
|
@@ -431,3 +539,22 @@ export const EXTENSION_DATA_HOOK_PERMISSIONS = {
|
|
|
431
539
|
} as const satisfies Readonly<Record<string, HostUiPermission>>
|
|
432
540
|
|
|
433
541
|
export type ExtensionDataHookName = keyof typeof EXTENSION_DATA_HOOK_PERMISSIONS
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* The capability level a set of permissions needs: Manifest V7 itself, or the level that introduced the newest
|
|
545
|
+
* capability it declares. Packaging and import compare it with `requires.sdk`, so an extension cannot claim a level
|
|
546
|
+
* too old for what it uses.
|
|
547
|
+
*/
|
|
548
|
+
export const requiredExtensionSdkLevel = (
|
|
549
|
+
permissions: ExtensionPermissions | undefined,
|
|
550
|
+
): { readonly level: number; readonly capabilities: readonly string[] } => {
|
|
551
|
+
const level8 = [
|
|
552
|
+
permissions?.host?.assets === undefined ? undefined : '扩展资源库',
|
|
553
|
+
permissions?.agent?.assets?.library === undefined ? undefined : '扩展资源库',
|
|
554
|
+
permissions?.host?.index === undefined && permissions?.agent?.index === undefined ? undefined : '检索索引',
|
|
555
|
+
permissions?.host?.models === undefined ? undefined : '本机层模型调用',
|
|
556
|
+
].filter((name): name is string => name !== undefined)
|
|
557
|
+
return level8.length > 0
|
|
558
|
+
? { level: 8, capabilities: [...new Set(level8)] }
|
|
559
|
+
: { level: EXTENSION_SDK_BASE_LEVEL, capabilities: [] }
|
|
560
|
+
}
|