@nekro-nxt/contracts 0.4.0 → 0.6.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 (50) hide show
  1. package/README.md +2 -0
  2. package/dist/index.mjs +3092 -164
  3. package/dist/index.mjs.map +1 -1
  4. package/lib/types/commands.d.ts +408 -0
  5. package/lib/types/commands.d.ts.map +1 -0
  6. package/lib/types/community-feedback-host.d.ts +442 -0
  7. package/lib/types/community-feedback-host.d.ts.map +1 -0
  8. package/lib/types/community-feedback.d.ts +1072 -0
  9. package/lib/types/community-feedback.d.ts.map +1 -0
  10. package/lib/types/community-personas.d.ts +4 -0
  11. package/lib/types/community-personas.d.ts.map +1 -1
  12. package/lib/types/community.d.ts +260 -13
  13. package/lib/types/community.d.ts.map +1 -1
  14. package/lib/types/config-schema.d.ts +16 -2
  15. package/lib/types/config-schema.d.ts.map +1 -1
  16. package/lib/types/credentials.d.ts +328 -0
  17. package/lib/types/credentials.d.ts.map +1 -0
  18. package/lib/types/domain.d.ts +44 -2
  19. package/lib/types/domain.d.ts.map +1 -1
  20. package/lib/types/extension-capabilities.d.ts +58 -3
  21. package/lib/types/extension-capabilities.d.ts.map +1 -1
  22. package/lib/types/extension-ui.d.ts +46 -4
  23. package/lib/types/extension-ui.d.ts.map +1 -1
  24. package/lib/types/host-api.d.ts +10805 -2887
  25. package/lib/types/host-api.d.ts.map +1 -1
  26. package/lib/types/index.d.ts +7 -0
  27. package/lib/types/index.d.ts.map +1 -1
  28. package/lib/types/linked-accounts.d.ts +100 -0
  29. package/lib/types/linked-accounts.d.ts.map +1 -0
  30. package/lib/types/logs.d.ts +355 -0
  31. package/lib/types/logs.d.ts.map +1 -0
  32. package/lib/types/response-timing.d.ts +60 -0
  33. package/lib/types/response-timing.d.ts.map +1 -0
  34. package/lib/types/runtime-compatibility.d.ts +2 -2
  35. package/package.json +1 -1
  36. package/src/commands.ts +366 -0
  37. package/src/community-feedback-host.ts +121 -0
  38. package/src/community-feedback.ts +289 -0
  39. package/src/community.ts +63 -1
  40. package/src/config-schema.ts +33 -2
  41. package/src/credentials.ts +260 -0
  42. package/src/domain.ts +74 -8
  43. package/src/extension-capabilities.ts +78 -4
  44. package/src/extension-ui.ts +3 -0
  45. package/src/host-api.ts +1873 -163
  46. package/src/index.ts +8 -0
  47. package/src/linked-accounts.ts +77 -0
  48. package/src/logs.ts +138 -0
  49. package/src/management-api.ts +1 -1
  50. package/src/response-timing.ts +77 -0
@@ -0,0 +1,260 @@
1
+ import { z } from 'zod'
2
+ import { AgentIdSchema, ChannelIdSchema, CredentialIdSchema, ExtensionIdSchema } from './domain.js'
3
+ import { ExtensionNetworkDomainSchema } from './extension-capabilities.js'
4
+
5
+ /**
6
+ * Named credentials (智能体扩展配置与凭据 C1–C5): one entry is added once and reused by extensions, MCP servers and
7
+ * commands. The value is write-only; every view below carries metadata only. Accounts on platforms such as GitHub are
8
+ * linked accounts (`linked-accounts.ts`), granted per agent, not credentials.
9
+ */
10
+
11
+ export const CREDENTIAL_NAME_MAX_CHARS = 64
12
+ export const CREDENTIAL_DESCRIPTION_MAX_CHARS = 500
13
+ export const CREDENTIAL_VALUE_MAX_CHARS = 16_384
14
+ /** Values shorter than this are not hidden from tool results; the form warns when one is saved. */
15
+ export const CREDENTIAL_REDACTABLE_MIN_CHARS = 8
16
+
17
+ /** How a credential reaches code: `request` only as a placeholder filled by `nxt.http.fetch`; `program` as the value. */
18
+ export const CredentialUsageSchema = z.enum(['request', 'program'])
19
+ export type CredentialUsage = z.output<typeof CredentialUsageSchema>
20
+
21
+ /** Agents that may use a credential: every agent, or the listed ones. */
22
+ export const CredentialAgentScopeSchema = z.union([
23
+ z.object({ all: z.literal(true) }).strict(),
24
+ z.object({ agentIds: z.array(AgentIdSchema).max(256) }).strict(),
25
+ ])
26
+ export type CredentialAgentScope = z.output<typeof CredentialAgentScopeSchema>
27
+
28
+ /** Shown in redaction markers (`[凭据:名称]`), so brackets and control characters are not allowed. */
29
+ export const CredentialNameSchema = z
30
+ .string()
31
+ .trim()
32
+ .min(1, '名称不能为空。')
33
+ .max(CREDENTIAL_NAME_MAX_CHARS, `名称最多 ${CREDENTIAL_NAME_MAX_CHARS} 个字符。`)
34
+ // eslint-disable-next-line no-control-regex -- Control characters are exactly what the name must not contain.
35
+ .regex(/^[^\u0000-\u001f\u007f[\]]+$/u, '名称不能包含方括号或控制字符。')
36
+
37
+ export const CredentialDescriptionSchema = z
38
+ .string()
39
+ .trim()
40
+ .max(CREDENTIAL_DESCRIPTION_MAX_CHARS, `说明最多 ${CREDENTIAL_DESCRIPTION_MAX_CHARS} 个字符。`)
41
+
42
+ export const CredentialDomainsSchema = z.array(ExtensionNetworkDomainSchema).max(32)
43
+
44
+ /**
45
+ * Names a credential's environment variable cannot take: the shell, the tools' own configuration, and the variables
46
+ * linked accounts give commands (`GH_TOKEN` and the `gh` settings).
47
+ */
48
+ const RESERVED_COMMAND_ENV = new Set([
49
+ 'PATH',
50
+ 'HOME',
51
+ 'USER',
52
+ 'LOGNAME',
53
+ 'SHELL',
54
+ 'PWD',
55
+ 'OLDPWD',
56
+ 'TMPDIR',
57
+ 'TEMP',
58
+ 'TMP',
59
+ 'LANG',
60
+ 'TERM',
61
+ 'NODE_OPTIONS',
62
+ 'NODE_PATH',
63
+ 'GH_TOKEN',
64
+ 'GH_CONFIG_DIR',
65
+ 'GH_PROMPT_DISABLED',
66
+ 'GH_NO_UPDATE_NOTIFIER',
67
+ 'GH_NO_EXTENSION_UPDATE_NOTIFIER',
68
+ 'GIT_TERMINAL_PROMPT',
69
+ 'GIT_CONFIG_COUNT',
70
+ ])
71
+ const RESERVED_COMMAND_ENV_PREFIXES = ['LC_', 'LD_', 'DYLD_', 'GIT_CONFIG_', 'NEKRO_', 'DSH_']
72
+
73
+ /** The environment variable a program credential reaches commands under. */
74
+ export const CredentialEnvNameSchema = z
75
+ .string()
76
+ .trim()
77
+ .max(64)
78
+ .regex(/^[A-Z_][A-Z0-9_]*$/u, '环境变量名只能使用大写字母、数字和下划线,且不能以数字开头。')
79
+ .refine(
80
+ (name) =>
81
+ !RESERVED_COMMAND_ENV.has(name) && !RESERVED_COMMAND_ENV_PREFIXES.some((prefix) => name.startsWith(prefix)),
82
+ '这个环境变量名由系统使用,请换一个。',
83
+ )
84
+
85
+ export const CredentialValueSchema = z.string().min(1).max(CREDENTIAL_VALUE_MAX_CHARS)
86
+
87
+ /** What uses one credential right now: extension secret fields bound to it and agents whose commands receive it. */
88
+ export const CredentialUsersSchema = z
89
+ .object({
90
+ extensions: z.array(
91
+ z
92
+ .object({
93
+ extensionId: ExtensionIdSchema,
94
+ name: z.string(),
95
+ /** Agents whose own configuration binds it; empty when only the host configuration does. */
96
+ agentIds: z.array(AgentIdSchema),
97
+ host: z.boolean(),
98
+ })
99
+ .strict(),
100
+ ),
101
+ /** Agents with commands enabled whose commands receive it. */
102
+ commandAgents: z.array(AgentIdSchema),
103
+ })
104
+ .strict()
105
+ export type CredentialUsers = z.output<typeof CredentialUsersSchema>
106
+
107
+ export const CredentialViewSchema = z
108
+ .object({
109
+ id: CredentialIdSchema,
110
+ name: CredentialNameSchema,
111
+ description: z.string(),
112
+ usage: CredentialUsageSchema,
113
+ domains: z.array(z.string()),
114
+ agentScope: CredentialAgentScopeSchema,
115
+ autoUse: z.boolean(),
116
+ /** Program credentials: the environment variable agents' commands receive it under, or `null` when they do not. */
117
+ envName: z.string().nullable(),
118
+ /** Agents an administrator let use it while automatic use is off. */
119
+ approvedAgentIds: z.array(AgentIdSchema),
120
+ /** False when the stored value is too short to hide from tool results, or cannot be read. */
121
+ redactable: z.boolean(),
122
+ /** False when the stored value cannot be read any more; the entry stays so it can be replaced. */
123
+ available: z.boolean(),
124
+ usedBy: CredentialUsersSchema,
125
+ createdAt: z.number().int().nonnegative(),
126
+ updatedAt: z.number().int().nonnegative(),
127
+ lastUsedAt: z.number().int().nonnegative().nullable(),
128
+ })
129
+ .strict()
130
+ export type CredentialView = z.output<typeof CredentialViewSchema>
131
+
132
+ /** A model provider listed with the credentials; `open` lets agents and extensions call it through the host. */
133
+ export const ModelProviderAccessSchema = z
134
+ .object({
135
+ provider: z.string().min(1),
136
+ name: z.string(),
137
+ open: z.boolean(),
138
+ })
139
+ .strict()
140
+ export type ModelProviderAccess = z.output<typeof ModelProviderAccessSchema>
141
+
142
+ /**
143
+ * An agent asked for a credential that does not exist yet. The settings link pre-fills the add form; saving a
144
+ * credential with this name wakes the agent in the channel it asked from.
145
+ */
146
+ export const CredentialRequestViewSchema = z
147
+ .object({
148
+ id: z.string().min(1).max(64),
149
+ name: CredentialNameSchema,
150
+ purpose: z.string(),
151
+ domains: z.array(z.string()),
152
+ usage: CredentialUsageSchema,
153
+ agentId: AgentIdSchema,
154
+ channelId: ChannelIdSchema.nullable(),
155
+ createdAt: z.number().int().nonnegative(),
156
+ /** Settings route that opens the add form pre-filled, see {@link credentialRequestRoute}. */
157
+ route: z.string(),
158
+ })
159
+ .strict()
160
+ export type CredentialRequestView = z.output<typeof CredentialRequestViewSchema>
161
+
162
+ export const CredentialsViewSchema = z
163
+ .object({
164
+ credentials: z.array(CredentialViewSchema),
165
+ modelProviders: z.array(ModelProviderAccessSchema),
166
+ requests: z.array(CredentialRequestViewSchema),
167
+ })
168
+ .strict()
169
+ export type CredentialsView = z.output<typeof CredentialsViewSchema>
170
+
171
+ export const CreateCredentialRequestSchema = z
172
+ .object({
173
+ name: CredentialNameSchema,
174
+ description: CredentialDescriptionSchema.default(''),
175
+ agentScope: CredentialAgentScopeSchema.default({ all: true }),
176
+ autoUse: z.boolean().default(true),
177
+ usage: CredentialUsageSchema.default('request'),
178
+ domains: CredentialDomainsSchema.default([]),
179
+ /** Program credentials only. */
180
+ envName: CredentialEnvNameSchema.optional(),
181
+ value: CredentialValueSchema,
182
+ /** The agent request this form was opened from, if any. */
183
+ requestId: z.string().min(1).max(64).optional(),
184
+ })
185
+ .strict()
186
+ export type CreateCredentialRequest = z.input<typeof CreateCredentialRequestSchema>
187
+
188
+ /** Metadata changes; omitted fields keep their value. */
189
+ export const UpdateCredentialRequestSchema = z
190
+ .object({
191
+ name: CredentialNameSchema.optional(),
192
+ description: CredentialDescriptionSchema.optional(),
193
+ usage: CredentialUsageSchema.optional(),
194
+ domains: CredentialDomainsSchema.optional(),
195
+ agentScope: CredentialAgentScopeSchema.optional(),
196
+ autoUse: z.boolean().optional(),
197
+ /** `null` stops giving it to commands; only program credentials take one. */
198
+ envName: CredentialEnvNameSchema.nullable().optional(),
199
+ /** Replaces the agents allowed to use it while automatic use is off, for example to withdraw an approval. */
200
+ approvedAgentIds: z.array(AgentIdSchema).max(256).optional(),
201
+ })
202
+ .strict()
203
+ export type UpdateCredentialRequest = z.output<typeof UpdateCredentialRequestSchema>
204
+
205
+ export const ReplaceCredentialValueRequestSchema = z.object({ value: CredentialValueSchema }).strict()
206
+ export type ReplaceCredentialValueRequest = z.output<typeof ReplaceCredentialValueRequestSchema>
207
+
208
+ /** Removing a credential that is in use needs `confirm`; without it the result lists who uses it. */
209
+ export const RemoveCredentialRequestSchema = z.object({ confirm: z.boolean().default(false) }).strict()
210
+
211
+ export const RemoveCredentialResultSchema = z.discriminatedUnion('removed', [
212
+ z.object({ removed: z.literal(true), view: CredentialsViewSchema }).strict(),
213
+ z.object({ removed: z.literal(false), usedBy: CredentialUsersSchema }).strict(),
214
+ ])
215
+ export type RemoveCredentialResult = z.output<typeof RemoveCredentialResultSchema>
216
+
217
+ export const CredentialConsumerKindSchema = z.enum(['extension', 'command', 'mcp', 'model'])
218
+
219
+ /** One recorded use. The newest 500 per credential are kept, none older than 90 days. */
220
+ export const CredentialUseSchema = z
221
+ .object({
222
+ at: z.number().int().nonnegative(),
223
+ agentId: AgentIdSchema.nullable(),
224
+ channelId: ChannelIdSchema.nullable(),
225
+ requesterMemberId: z.string().nullable(),
226
+ consumerKind: CredentialConsumerKindSchema,
227
+ consumerId: z.string(),
228
+ /** Extension display name or MCP server name, when it is still known. */
229
+ consumerName: z.string().nullable(),
230
+ targetHost: z.string().nullable(),
231
+ })
232
+ .strict()
233
+ export type CredentialUse = z.output<typeof CredentialUseSchema>
234
+
235
+ export const CredentialUsesViewSchema = z.object({ uses: z.array(CredentialUseSchema) }).strict()
236
+
237
+ export const SetModelProviderAccessRequestSchema = z.object({ open: z.boolean() }).strict()
238
+
239
+ /**
240
+ * The settings route that opens the add form pre-filled from an agent's request: the credential section with `add=1`
241
+ * plus `name`, `purpose`, `domains` (comma separated), `usage` and `request` (the request id) as query parameters.
242
+ */
243
+ export const credentialRequestRoute = (request: {
244
+ readonly id: string
245
+ readonly name: string
246
+ readonly purpose: string
247
+ readonly domains: readonly string[]
248
+ readonly usage: CredentialUsage
249
+ }): string => {
250
+ const query = new URLSearchParams({ add: '1', name: request.name, request: request.id, usage: request.usage })
251
+ if (request.purpose !== '') query.set('purpose', request.purpose)
252
+ if (request.domains.length > 0) query.set('domains', request.domains.join(','))
253
+ return `/settings/credentials?${query.toString()}`
254
+ }
255
+
256
+ /** Secret fields of an extension configuration layer bound to a named credential, by field. */
257
+ export const BoundCredentialsSchema = z.record(z.string(), CredentialIdSchema)
258
+
259
+ /** Binds secret fields to named credentials; `null` removes the binding and leaves the field unset. */
260
+ export const CredentialBindingsRequestSchema = z.record(z.string(), CredentialIdSchema.nullable())
package/src/domain.ts CHANGED
@@ -1,5 +1,35 @@
1
1
  import { z } from 'zod'
2
2
 
3
+ /** Rendered send-tool result consumed by both the agent and the product detail view. */
4
+ export const MessageDeliverySummarySchema = z
5
+ .object({
6
+ status: z.enum(['sent', 'partially-sent', 'failed', 'unknown']),
7
+ issues: z
8
+ .array(
9
+ z
10
+ .object({
11
+ status: z.enum(['failed', 'unknown']),
12
+ message: z.string().max(1000),
13
+ kind: z.enum(['transient', 'permanent', 'rate-limited', 'authentication', 'invalid']).optional(),
14
+ action: z.string().max(120).optional(),
15
+ retcode: z.number().int().safe().optional(),
16
+ })
17
+ .strict(),
18
+ )
19
+ .max(20),
20
+ omittedIssues: z.number().int().nonnegative().optional(),
21
+ /** Extensions that changed the message before it was sent, the content they produced and what they appended. */
22
+ processing: z
23
+ .object({
24
+ processedBy: z.array(z.string()),
25
+ parts: z.array(z.unknown()).optional(),
26
+ followUps: z.array(z.string()).optional(),
27
+ })
28
+ .strict()
29
+ .optional(),
30
+ })
31
+ .strict()
32
+
3
33
  const brandedId = <Prefix extends string, Brand extends string>(prefix: Prefix, brand: Brand) =>
4
34
  z
5
35
  .string()
@@ -28,8 +58,14 @@ export const DshPluginEntryIdSchema = brandedId('dse', 'DshPluginEntryId')
28
58
  export const HostUiPageInstanceIdSchema = brandedId('hup', 'HostUiPageInstanceId')
29
59
  export const AuthoringTaskIdSchema = brandedId('aut', 'AuthoringTaskId')
30
60
  export const AuthoringAttemptIdSchema = brandedId('aua', 'AuthoringAttemptId')
61
+ export const SubConversationIdSchema = brandedId('cnv', 'SubConversationId')
62
+ export const CredentialIdSchema = brandedId('crd', 'CredentialId')
63
+ export const LinkedAccountIdSchema = brandedId('acc', 'LinkedAccountId')
31
64
 
32
65
  export type AgentId = z.infer<typeof AgentIdSchema>
66
+ export type SubConversationId = z.infer<typeof SubConversationIdSchema>
67
+ export type CredentialId = z.infer<typeof CredentialIdSchema>
68
+ export type LinkedAccountId = z.infer<typeof LinkedAccountIdSchema>
33
69
  export type AgentRevisionId = z.infer<typeof AgentRevisionIdSchema>
34
70
  export type ConnectionId = z.infer<typeof ConnectionIdSchema>
35
71
  export type ConnectionEventId = z.infer<typeof ConnectionEventIdSchema>
@@ -205,8 +241,8 @@ export interface DshPluginActivationRecord {
205
241
  export interface DshPluginDiagnosticRecord {
206
242
  readonly entryId: DshPluginEntryId
207
243
  readonly targetKey: string
208
- readonly status: 'active' | 'load-failed' | 'restore-failed' | 'dispose-failed'
209
- readonly phase: 'import' | 'apply' | 'update' | 'dispose' | 'restore'
244
+ readonly status: 'active' | 'pending' | 'load-failed' | 'restore-failed' | 'dispose-failed'
245
+ readonly phase: 'import' | 'dependency' | 'apply' | 'update' | 'dispose' | 'restore'
210
246
  readonly message?: string
211
247
  readonly observedAt: number
212
248
  }
@@ -259,7 +295,18 @@ export const RichTargetUrlSchema = z
259
295
  export const MessagePartSchema = z.discriminatedUnion('type', [
260
296
  z.object({ type: z.literal('text'), text: z.string() }).strict(),
261
297
  z.object({ type: z.literal('mention'), memberId: ChannelMemberIdSchema }).strict(),
262
- z.object({ type: z.literal('image'), assetId: AssetIdSchema, alt: z.string().optional() }).strict(),
298
+ z
299
+ .object({
300
+ type: z.literal('image'),
301
+ assetId: AssetIdSchema,
302
+ alt: z.string().optional(),
303
+ /**
304
+ * The platform reported the picture as a sticker. A hint only: a sticker dragged in from a desktop client and
305
+ * pictures from platforms without this signal arrive unmarked.
306
+ */
307
+ sticker: z.literal(true).optional(),
308
+ })
309
+ .strict(),
263
310
  z.object({ type: z.literal('file'), assetId: AssetIdSchema, name: z.string().optional() }).strict(),
264
311
  z.object({ type: z.literal('audio'), assetId: AssetIdSchema }).strict(),
265
312
  z.object({ type: z.literal('quote'), messageId: LogicalMessageIdSchema }).strict(),
@@ -287,19 +334,24 @@ export function parseMessageParts(input: unknown): MessagePart[] {
287
334
  return MessagePartsSchema.parse(input)
288
335
  }
289
336
 
290
- const collectRichAssetIds = (value: JsonValue | undefined, into: AssetId[]): void => {
337
+ const collectRichAssetIds = (value: JsonValue | undefined, into: AssetId[], imagesOnly = false): void => {
291
338
  if (value === null || value === undefined || typeof value === 'boolean' || typeof value === 'number') return
292
339
  if (typeof value === 'string') return
293
340
  if (Array.isArray(value)) {
294
- for (const item of value) collectRichAssetIds(item, into)
341
+ for (const item of value) collectRichAssetIds(item, into, imagesOnly)
295
342
  return
296
343
  }
297
344
  for (const [key, child] of Object.entries(value)) {
298
- if ((key === 'imageAssetId' || key === 'previewAssetId') && typeof child === 'string') {
345
+ if (
346
+ (key === 'imageAssetId' ||
347
+ key === 'previewAssetId' ||
348
+ (!imagesOnly && (key === 'audioAssetId' || key === 'fileAssetId'))) &&
349
+ typeof child === 'string'
350
+ ) {
299
351
  const parsed = AssetIdSchema.safeParse(child)
300
352
  if (parsed.success) into.push(parsed.data)
301
353
  } else {
302
- collectRichAssetIds(child, into)
354
+ collectRichAssetIds(child, into, imagesOnly)
303
355
  }
304
356
  }
305
357
  }
@@ -315,6 +367,15 @@ export const messagePartAssetIds = (part: MessagePart): readonly AssetId[] => {
315
367
 
316
368
  export const messagePartAssetId = (part: MessagePart): AssetId | undefined => messagePartAssetIds(part)[0]
317
369
 
370
+ /** Only actual pictures enter visual context; forwarded files and recordings retain their own media kind. */
371
+ export const messagePartImageAssetIds = (part: MessagePart): readonly AssetId[] => {
372
+ if (part.type === 'image') return [part.assetId]
373
+ if (part.type !== 'rich') return []
374
+ const ids: AssetId[] = part.previewAssetId ? [part.previewAssetId] : []
375
+ collectRichAssetIds(part.extension, ids, true)
376
+ return ids
377
+ }
378
+
318
379
  const asJsonRecord = (value: JsonValue | undefined): Readonly<Record<string, JsonValue>> | undefined =>
319
380
  value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined
320
381
 
@@ -332,7 +393,12 @@ const richItemLine = (value: JsonValue): string | undefined => {
332
393
  : undefined
333
394
  const imageName = typeof item['imageName'] === 'string' ? item['imageName'] : undefined
334
395
  const imageText = typeof item['imageAssetId'] === 'string' ? `[图片${imageName ? ` ${imageName}` : ''}]` : undefined
335
- const body = [text, cardSummary ? `[卡片] ${cardSummary}` : undefined, imageText]
396
+ const audioText = typeof item['audioAssetId'] === 'string' ? `[语音 ${item['audioAssetId']}]` : undefined
397
+ const fileText =
398
+ typeof item['fileAssetId'] === 'string'
399
+ ? `[${typeof item['fileMediaType'] === 'string' && item['fileMediaType'].startsWith('video/') ? '视频' : '文件'} ${item['fileAssetId']}${typeof item['fileName'] === 'string' ? `:${item['fileName']}` : ''}]`
400
+ : undefined
401
+ const body = [text, cardSummary ? `[卡片] ${cardSummary}` : undefined, imageText, audioText, fileText]
336
402
  .filter((part): part is string => Boolean(part && part.length > 0))
337
403
  .join(' ')
338
404
  if (!sender && !body) return undefined
@@ -4,9 +4,18 @@ import { z } from 'zod'
4
4
  * Host capability level this build implements. A Revision that declares `requires.sdk` above it is rejected at
5
5
  * import with an "upgrade NekroNXT" message instead of an opaque schema error. Bump when a Manifest capability
6
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
+ * images, the search index and host-layer model calls. Level 9 adds the structured logger and explicit chat commands. Level 10 adds version lineage, the data version with its
8
+ * migration hook and administrator-only commands. Level 11 lists the pictures of an inbound message to inbound hooks.
9
+ * Level 12 adds credential placeholders (`secrets.placeholder`) that `http.fetch` fills for the credential's domains.
10
+ * Level 13 adds outbound processing (`harness.onOutbound`, `permissions.agent.outbound`).
8
11
  */
9
- export const EXTENSION_SDK_LEVEL = 8
12
+ export const EXTENSION_SDK_LEVEL = 13
13
+
14
+ /** Outbound processing needs this level; packages that declare `permissions.agent.outbound` declare it. */
15
+ export const EXTENSION_OUTBOUND_SDK_LEVEL = 13
16
+
17
+ /** Lineage, `dataVersion` and administrator-only commands need this level; packages that carry them declare it. */
18
+ export const EXTENSION_GOVERNANCE_SDK_LEVEL = 10
10
19
 
11
20
  /**
12
21
  * The first NekroNXT version of each capability level, so users read "needs NekroNXT 0.5.0" rather than a level
@@ -16,6 +25,11 @@ export const EXTENSION_SDK_RELEASES: readonly { readonly level: number; readonly
16
25
  { level: 6, version: '0.3.0' },
17
26
  { level: 7, version: '0.4.0' },
18
27
  { level: 8, version: '0.5.0' },
28
+ { level: 9, version: '0.6.0' },
29
+ { level: 10, version: '0.6.0' },
30
+ { level: 11, version: '0.6.0' },
31
+ { level: 12, version: '0.7.0' },
32
+ { level: 13, version: '0.7.0' },
19
33
  ]
20
34
 
21
35
  /** The oldest NekroNXT version that supports `level`; undefined for a level no release supports yet. */
@@ -121,6 +135,32 @@ export const ExtensionInboundHookCapabilitySchema = z
121
135
  .strict()
122
136
  export type ExtensionInboundHookCapability = z.output<typeof ExtensionInboundHookCapabilitySchema>
123
137
 
138
+ export const EXTENSION_OUTBOUND_MAX_TIMEOUT_MS = 15_000
139
+
140
+ /**
141
+ * Outbound processing of the agent's own messages. `read` sees each message after it was delivered; `transform` may
142
+ * replace its content or append messages before it is sent; `cancel` may stop it. `onTimeout` decides what happens
143
+ * when the handler times out or throws before sending: `send` (default) sends the message as it is.
144
+ */
145
+ export const ExtensionOutboundCapabilitySchema = z
146
+ .object({
147
+ read: z.literal(true).optional(),
148
+ transform: z.literal(true).optional(),
149
+ cancel: z.literal(true).optional(),
150
+ timeoutMs: z.number().int().min(100).max(EXTENSION_OUTBOUND_MAX_TIMEOUT_MS),
151
+ onTimeout: z.enum(['send', 'cancel']).optional(),
152
+ })
153
+ .strict()
154
+ .refine(
155
+ (value) => value.read === true || value.transform === true || value.cancel === true,
156
+ 'outbound 至少声明 read、transform 或 cancel 中的一项。',
157
+ )
158
+ export type ExtensionOutboundCapability = z.output<typeof ExtensionOutboundCapabilitySchema>
159
+
160
+ /** Whether the extension takes part before sending (and so in the agent's processing order). */
161
+ export const outboundProcessesBeforeSend = (outbound: ExtensionOutboundCapability | undefined): boolean =>
162
+ outbound?.transform === true || outbound?.cancel === true
163
+
124
164
  export const EXTENSION_JOB_MAX_RUNTIME = 50
125
165
 
126
166
  export const ExtensionDeclaredJobSchema = z
@@ -248,10 +288,14 @@ export const ExtensionModelsCapabilitySchema = z
248
288
  .strict()
249
289
  export type ExtensionModelsCapability = z.output<typeof ExtensionModelsCapabilitySchema>
250
290
 
251
- /** A header or environment value: literal text, or the current value of a `meta.role: 'secret'` config field. */
291
+ /**
292
+ * A header or environment value: literal text, the current value of a `meta.role: 'secret'` config field, or a named
293
+ * credential (by id or name) that the agent may use.
294
+ */
252
295
  export const ExtensionMcpValueSchema = z.union([
253
296
  z.string().max(4096),
254
297
  z.object({ secret: z.string().trim().min(1).max(64) }).strict(),
298
+ z.object({ credential: z.string().trim().min(1).max(64) }).strict(),
255
299
  ])
256
300
  export type ExtensionMcpValue = z.output<typeof ExtensionMcpValueSchema>
257
301
 
@@ -308,6 +352,8 @@ const McpFormValueSchema = z
308
352
  name: z.string().regex(/^[A-Za-z_][A-Za-z0-9_-]{0,127}$/u, '名称只能包含字母、数字、下划线和连字符。'),
309
353
  value: z.string().max(4096),
310
354
  secret: z.boolean().default(false),
355
+ /** Takes the value from this named credential instead; `value` and `secret` are then ignored. */
356
+ credential: z.string().trim().min(1).max(64).optional(),
311
357
  })
312
358
  .strict()
313
359
 
@@ -337,7 +383,7 @@ export const mcpSecretFields = (mcp: ExtensionMcpCapability | undefined): readon
337
383
  ...new Set(
338
384
  (mcp?.servers ?? []).flatMap((server) =>
339
385
  Object.values(server.transport === 'stdio' ? server.env : server.headers).flatMap((value) =>
340
- typeof value === 'string' ? [] : [value.secret],
386
+ typeof value === 'string' || !('secret' in value) ? [] : [value.secret],
341
387
  ),
342
388
  ),
343
389
  ),
@@ -366,6 +412,7 @@ export const ExtensionCapabilitiesSchema = z
366
412
  .strict()
367
413
  .optional(),
368
414
  inboundHook: ExtensionInboundHookCapabilitySchema.optional(),
415
+ outbound: ExtensionOutboundCapabilitySchema.optional(),
369
416
  jobs: ExtensionJobsCapabilitySchema.optional(),
370
417
  platform: ExtensionPlatformCapabilitySchema.optional(),
371
418
  llm: ExtensionLlmCapabilitySchema.optional(),
@@ -477,6 +524,18 @@ export const extensionCapabilitiesExpand = (
477
524
  ) {
478
525
  return true
479
526
  }
527
+ const outbound = next.outbound
528
+ const previousOutbound = previous?.outbound
529
+ if (
530
+ outbound !== undefined &&
531
+ (previousOutbound === undefined ||
532
+ (outbound.read === true && previousOutbound.read !== true) ||
533
+ (outbound.transform === true && previousOutbound.transform !== true) ||
534
+ (outbound.cancel === true && previousOutbound.cancel !== true) ||
535
+ (outbound.onTimeout === 'cancel' && previousOutbound.onTimeout !== 'cancel'))
536
+ ) {
537
+ return true
538
+ }
480
539
  if (
481
540
  next.jobs?.runtime !== undefined &&
482
541
  (previous?.jobs?.runtime === undefined || next.jobs.runtime.maxActive > previous.jobs.runtime.maxActive)
@@ -591,6 +650,21 @@ export const summarizeExtensionCapabilities = (
591
650
  ...(effects === '' ? {} : { detail: `可以${effects}` }),
592
651
  })
593
652
  }
653
+ const outbound = capabilities.outbound
654
+ if (outbound !== undefined) {
655
+ if (outbound.transform === true) {
656
+ items.push({ key: 'outbound.transform', risk: 'high', label: '在智能体的消息发出前修改内容或追加消息' })
657
+ }
658
+ if (outbound.cancel === true) {
659
+ items.push({ key: 'outbound.cancel', risk: 'high', label: '取消智能体要发出的消息' })
660
+ }
661
+ if (outbound.read === true) {
662
+ items.push({ key: 'outbound.read', risk: 'sensitive', label: '查看智能体发出的每条消息' })
663
+ }
664
+ if (outbound.onTimeout === 'cancel' && (outbound.transform === true || outbound.cancel === true)) {
665
+ items.push({ key: 'outbound.onTimeout', risk: 'high', label: '这个扩展出错时,智能体的消息会发不出去' })
666
+ }
667
+ }
594
668
  const jobs = capabilities.jobs
595
669
  if (jobs?.declared !== undefined && jobs.declared.length > 0) {
596
670
  items.push({
@@ -4,6 +4,7 @@ import {
4
4
  HostLayerCapabilitiesSchema,
5
5
  hostLayerAsCapabilities,
6
6
  EXTENSION_SDK_BASE_LEVEL,
7
+ EXTENSION_OUTBOUND_SDK_LEVEL,
7
8
  } from './extension-capabilities.js'
8
9
  import { ExtensionIdSchema, ExtensionRevisionIdSchema, HostUiPageInstanceIdSchema } from './domain.js'
9
10
  import { ConfigSchemaDocumentSchema } from './config-schema.js'
@@ -554,6 +555,8 @@ export const requiredExtensionSdkLevel = (
554
555
  permissions?.host?.index === undefined && permissions?.agent?.index === undefined ? undefined : '检索索引',
555
556
  permissions?.host?.models === undefined ? undefined : '本机层模型调用',
556
557
  ].filter((name): name is string => name !== undefined)
558
+ if (permissions?.agent?.outbound !== undefined)
559
+ return { level: EXTENSION_OUTBOUND_SDK_LEVEL, capabilities: ['出站处理'] }
557
560
  return level8.length > 0
558
561
  ? { level: 8, capabilities: [...new Set(level8)] }
559
562
  : { level: EXTENSION_SDK_BASE_LEVEL, capabilities: [] }