@nekro-nxt/contracts 0.5.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.
@@ -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
@@ -18,6 +18,15 @@ export const MessageDeliverySummarySchema = z
18
18
  )
19
19
  .max(20),
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(),
21
30
  })
22
31
  .strict()
23
32
 
@@ -50,9 +59,13 @@ export const HostUiPageInstanceIdSchema = brandedId('hup', 'HostUiPageInstanceId
50
59
  export const AuthoringTaskIdSchema = brandedId('aut', 'AuthoringTaskId')
51
60
  export const AuthoringAttemptIdSchema = brandedId('aua', 'AuthoringAttemptId')
52
61
  export const SubConversationIdSchema = brandedId('cnv', 'SubConversationId')
62
+ export const CredentialIdSchema = brandedId('crd', 'CredentialId')
63
+ export const LinkedAccountIdSchema = brandedId('acc', 'LinkedAccountId')
53
64
 
54
65
  export type AgentId = z.infer<typeof AgentIdSchema>
55
66
  export type SubConversationId = z.infer<typeof SubConversationIdSchema>
67
+ export type CredentialId = z.infer<typeof CredentialIdSchema>
68
+ export type LinkedAccountId = z.infer<typeof LinkedAccountIdSchema>
56
69
  export type AgentRevisionId = z.infer<typeof AgentRevisionIdSchema>
57
70
  export type ConnectionId = z.infer<typeof ConnectionIdSchema>
58
71
  export type ConnectionEventId = z.infer<typeof ConnectionEventIdSchema>
@@ -228,8 +241,8 @@ export interface DshPluginActivationRecord {
228
241
  export interface DshPluginDiagnosticRecord {
229
242
  readonly entryId: DshPluginEntryId
230
243
  readonly targetKey: string
231
- readonly status: 'active' | 'load-failed' | 'restore-failed' | 'dispose-failed'
232
- 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'
233
246
  readonly message?: string
234
247
  readonly observedAt: number
235
248
  }
@@ -6,8 +6,13 @@ import { z } from 'zod'
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
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
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`).
9
11
  */
10
- export const EXTENSION_SDK_LEVEL = 11
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
11
16
 
12
17
  /** Lineage, `dataVersion` and administrator-only commands need this level; packages that carry them declare it. */
13
18
  export const EXTENSION_GOVERNANCE_SDK_LEVEL = 10
@@ -23,6 +28,8 @@ export const EXTENSION_SDK_RELEASES: readonly { readonly level: number; readonly
23
28
  { level: 9, version: '0.6.0' },
24
29
  { level: 10, version: '0.6.0' },
25
30
  { level: 11, version: '0.6.0' },
31
+ { level: 12, version: '0.7.0' },
32
+ { level: 13, version: '0.7.0' },
26
33
  ]
27
34
 
28
35
  /** The oldest NekroNXT version that supports `level`; undefined for a level no release supports yet. */
@@ -128,6 +135,32 @@ export const ExtensionInboundHookCapabilitySchema = z
128
135
  .strict()
129
136
  export type ExtensionInboundHookCapability = z.output<typeof ExtensionInboundHookCapabilitySchema>
130
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
+
131
164
  export const EXTENSION_JOB_MAX_RUNTIME = 50
132
165
 
133
166
  export const ExtensionDeclaredJobSchema = z
@@ -255,10 +288,14 @@ export const ExtensionModelsCapabilitySchema = z
255
288
  .strict()
256
289
  export type ExtensionModelsCapability = z.output<typeof ExtensionModelsCapabilitySchema>
257
290
 
258
- /** 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
+ */
259
295
  export const ExtensionMcpValueSchema = z.union([
260
296
  z.string().max(4096),
261
297
  z.object({ secret: z.string().trim().min(1).max(64) }).strict(),
298
+ z.object({ credential: z.string().trim().min(1).max(64) }).strict(),
262
299
  ])
263
300
  export type ExtensionMcpValue = z.output<typeof ExtensionMcpValueSchema>
264
301
 
@@ -315,6 +352,8 @@ const McpFormValueSchema = z
315
352
  name: z.string().regex(/^[A-Za-z_][A-Za-z0-9_-]{0,127}$/u, '名称只能包含字母、数字、下划线和连字符。'),
316
353
  value: z.string().max(4096),
317
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(),
318
357
  })
319
358
  .strict()
320
359
 
@@ -344,7 +383,7 @@ export const mcpSecretFields = (mcp: ExtensionMcpCapability | undefined): readon
344
383
  ...new Set(
345
384
  (mcp?.servers ?? []).flatMap((server) =>
346
385
  Object.values(server.transport === 'stdio' ? server.env : server.headers).flatMap((value) =>
347
- typeof value === 'string' ? [] : [value.secret],
386
+ typeof value === 'string' || !('secret' in value) ? [] : [value.secret],
348
387
  ),
349
388
  ),
350
389
  ),
@@ -373,6 +412,7 @@ export const ExtensionCapabilitiesSchema = z
373
412
  .strict()
374
413
  .optional(),
375
414
  inboundHook: ExtensionInboundHookCapabilitySchema.optional(),
415
+ outbound: ExtensionOutboundCapabilitySchema.optional(),
376
416
  jobs: ExtensionJobsCapabilitySchema.optional(),
377
417
  platform: ExtensionPlatformCapabilitySchema.optional(),
378
418
  llm: ExtensionLlmCapabilitySchema.optional(),
@@ -484,6 +524,18 @@ export const extensionCapabilitiesExpand = (
484
524
  ) {
485
525
  return true
486
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
+ }
487
539
  if (
488
540
  next.jobs?.runtime !== undefined &&
489
541
  (previous?.jobs?.runtime === undefined || next.jobs.runtime.maxActive > previous.jobs.runtime.maxActive)
@@ -598,6 +650,21 @@ export const summarizeExtensionCapabilities = (
598
650
  ...(effects === '' ? {} : { detail: `可以${effects}` }),
599
651
  })
600
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
+ }
601
668
  const jobs = capabilities.jobs
602
669
  if (jobs?.declared !== undefined && jobs.declared.length > 0) {
603
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: [] }