@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,289 @@
1
+ import { z } from 'zod'
2
+
3
+ export const COMMUNITY_FEEDBACK_STATUSES = [
4
+ 'new',
5
+ 'needs_info',
6
+ 'triaged',
7
+ 'planned',
8
+ 'fixed',
9
+ 'duplicate',
10
+ 'wontfix',
11
+ 'withdrawn',
12
+ ] as const
13
+ export const CommunityFeedbackStatusSchema = z.enum(COMMUNITY_FEEDBACK_STATUSES)
14
+ export type CommunityFeedbackStatus = z.output<typeof CommunityFeedbackStatusSchema>
15
+ export const COMMUNITY_FEEDBACK_STATUS_LABELS: Readonly<Record<CommunityFeedbackStatus, string>> = {
16
+ new: '新反馈',
17
+ needs_info: '需要补充',
18
+ triaged: '已确认',
19
+ planned: '已计划',
20
+ fixed: '已修复',
21
+ duplicate: '重复',
22
+ wontfix: '不处理',
23
+ withdrawn: '已撤回',
24
+ }
25
+ export const CommunityFeedbackKindSchema = z.enum(['bug', 'suggestion', 'question'])
26
+ export const CommunityFeedbackIdSchema = z
27
+ .string()
28
+ .regex(/^fbk_[0-9a-z]{26}$/u)
29
+ .brand<'CommunityFeedbackId'>()
30
+ export const CommunityFeedbackRefSchema = z.string().regex(/^FB-[1-9][0-9]*$/u)
31
+ export const CommunityFeedbackTargetSchema = z.union([
32
+ z.literal('nxt'),
33
+ z.object({ extensionId: z.string().regex(/^ext_[0-9A-Za-z]+$/u), version: z.string().max(120).optional() }).strict(),
34
+ ])
35
+ const version = z.string().trim().min(1).max(120)
36
+ const timestamp = z.number().int().nonnegative()
37
+
38
+ export const CommunityFeedbackExtensionDiagnosticSchema = z
39
+ .object({
40
+ extensionId: z.string().max(100),
41
+ version,
42
+ accessLevel: z.string().max(80).optional(),
43
+ activation: z.enum(['host', 'agent']).optional(),
44
+ })
45
+ .strict()
46
+ export const CommunityFeedbackDiagnosticEntrySchema = z
47
+ .object({
48
+ kind: z.enum(['compatibility', 'extension', 'connection']),
49
+ summary: z.string().max(2000),
50
+ extensionId: z.string().max(100).optional(),
51
+ })
52
+ .strict()
53
+
54
+ /** An explicit allowlist: credentials, account grants and arbitrary host configuration never cross this boundary. */
55
+ export const CommunityFeedbackDiagnosticsSchema = z
56
+ .object({
57
+ nxtVersion: version.optional(),
58
+ releaseChannel: z.enum(['stable', 'preview', 'development']).optional(),
59
+ hostKind: z.enum(['desktop', 'server']).optional(),
60
+ platform: z.string().max(80).optional(),
61
+ arch: z.string().max(40).optional(),
62
+ dshVersion: version.optional(),
63
+ adapters: z.array(z.string().max(100)).max(64).optional(),
64
+ agent: z
65
+ .object({
66
+ accessLevel: z.string().max(80),
67
+ subagents: z.boolean(),
68
+ fileTools: z.boolean(),
69
+ webSearch: z.boolean(),
70
+ dynamicCreation: z.boolean(),
71
+ developmentShell: z.boolean(),
72
+ unrestrictedFileAccess: z.boolean(),
73
+ scheduledTasks: z.boolean(),
74
+ codeRun: z.boolean(),
75
+ commandNetwork: z.boolean(),
76
+ communityFeedbackAuto: z.boolean(),
77
+ })
78
+ .strict()
79
+ .optional(),
80
+ extensions: z.array(CommunityFeedbackExtensionDiagnosticSchema).max(256).optional(),
81
+ entries: z.array(CommunityFeedbackDiagnosticEntrySchema).max(32).optional(),
82
+ instanceId: z.string().min(1).max(100).optional(),
83
+ })
84
+ .strict()
85
+ export type CommunityFeedbackDiagnostics = z.output<typeof CommunityFeedbackDiagnosticsSchema>
86
+
87
+ export const CommunityFeedbackExcerptSchema = z
88
+ .array(
89
+ z
90
+ .object({
91
+ speaker: z.string().max(80),
92
+ text: z.string().max(6000),
93
+ })
94
+ .strict(),
95
+ )
96
+ .max(8)
97
+ export type CommunityFeedbackExcerpt = z.output<typeof CommunityFeedbackExcerptSchema>
98
+
99
+ export const CommunityFeedbackCreateSchema = z
100
+ .object({
101
+ clientRequestId: z.string().min(1).max(160),
102
+ kind: CommunityFeedbackKindSchema,
103
+ target: CommunityFeedbackTargetSchema,
104
+ title: z.string().trim().min(1).max(120),
105
+ body: z.string().trim().min(1).max(8000),
106
+ diagnostics: CommunityFeedbackDiagnosticsSchema.default({}),
107
+ excerpt: CommunityFeedbackExcerptSchema.optional(),
108
+ excerptSharedWithAuthor: z.boolean().default(false),
109
+ source: z.enum(['assistant', 'assistant_auto', 'form', 'web']),
110
+ instanceId: z.string().min(1).max(100).optional(),
111
+ })
112
+ .strict()
113
+ export type CommunityFeedbackCreate = z.output<typeof CommunityFeedbackCreateSchema>
114
+ export const CommunityFeedbackMessageInputSchema = z
115
+ .object({
116
+ body: z.string().trim().min(1).max(4000),
117
+ audience: z.enum(['everyone', 'staff']).default('everyone'),
118
+ })
119
+ .strict()
120
+ export const CommunityFeedbackSupplementSchema = z
121
+ .object({
122
+ diagnostics: CommunityFeedbackDiagnosticsSchema.optional(),
123
+ excerpt: CommunityFeedbackExcerptSchema.optional(),
124
+ excerptSharedWithAuthor: z.boolean().optional(),
125
+ })
126
+ .strict()
127
+ export const CommunityFeedbackWithdrawSchema = z.object({ deleteText: z.boolean().default(false) }).strict()
128
+ export const CommunityFeedbackConfirmSchema = z.object({ clientVersion: version }).strict()
129
+ export const CommunityFeedbackReopenSchema = z.object({ note: z.string().trim().min(1).max(2000) }).strict()
130
+ export const CommunityFeedbackAffectedSchema = z
131
+ .object({
132
+ diagnostics: CommunityFeedbackDiagnosticsSchema.optional(),
133
+ nxtVersion: version.optional(),
134
+ })
135
+ .strict()
136
+ export const CommunityFeedbackTransitionSchema = z
137
+ .object({
138
+ status: CommunityFeedbackStatusSchema,
139
+ fixedIn: version.optional(),
140
+ duplicateOf: CommunityFeedbackRefSchema.optional(),
141
+ note: z.string().trim().max(2000).optional(),
142
+ })
143
+ .strict()
144
+ export type CommunityFeedbackTransition = z.output<typeof CommunityFeedbackTransitionSchema>
145
+ export const CommunityFeedbackLabelsSchema = z
146
+ .object({
147
+ labels: z.array(z.string().trim().min(1).max(40)).max(20),
148
+ priority: z.enum(['low', 'normal', 'high']).nullable(),
149
+ })
150
+ .strict()
151
+ export const CommunityFeedbackLinkSchema = z
152
+ .object({
153
+ githubUrl: z
154
+ .string()
155
+ .url()
156
+ .max(1000)
157
+ .refine((value) => /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/[1-9][0-9]*$/u.test(value))
158
+ .nullable(),
159
+ })
160
+ .strict()
161
+ export const CommunityFeedbackVisibilitySchema = z.object({ visibility: z.enum(['private', 'public']) }).strict()
162
+
163
+ export const CommunityFeedbackEventDataSchema = z
164
+ .object({
165
+ from: CommunityFeedbackStatusSchema.optional(),
166
+ to: CommunityFeedbackStatusSchema.optional(),
167
+ fixedIn: version.nullable().optional(),
168
+ duplicateOf: CommunityFeedbackRefSchema.nullable().optional(),
169
+ note: z.string().max(2000).optional(),
170
+ labels: z.array(z.string()).optional(),
171
+ priority: z.enum(['low', 'normal', 'high']).nullable().optional(),
172
+ visibility: z.enum(['private', 'public']).optional(),
173
+ clientVersion: version.optional(),
174
+ messageId: z.string().optional(),
175
+ audience: z.enum(['everyone', 'staff']).optional(),
176
+ deleteText: z.boolean().optional(),
177
+ affected: z.boolean().optional(),
178
+ })
179
+ .strict()
180
+ export const CommunityFeedbackEventSchema = z.object({
181
+ seq: z.number().int().positive(),
182
+ ref: CommunityFeedbackRefSchema,
183
+ type: z.enum([
184
+ 'created',
185
+ 'message',
186
+ 'status_changed',
187
+ 'labeled',
188
+ 'priority_changed',
189
+ 'linked_issue',
190
+ 'duplicate_marked',
191
+ 'fixed_in_set',
192
+ 'visibility_changed',
193
+ 'affected_added',
194
+ 'withdrawn',
195
+ 'reopened',
196
+ 'supplemented',
197
+ 'fix_confirmed',
198
+ ]),
199
+ actorKind: z.enum(['user', 'admin', 'author', 'token', 'system']).optional(),
200
+ actor: z.string().optional(),
201
+ tokenId: z.string().optional(),
202
+ data: CommunityFeedbackEventDataSchema,
203
+ createdAt: timestamp,
204
+ title: z.string().optional(),
205
+ status: CommunityFeedbackStatusSchema.optional(),
206
+ })
207
+ export type CommunityFeedbackEvent = z.output<typeof CommunityFeedbackEventSchema>
208
+ export const CommunityFeedbackMessageSchema = z.object({
209
+ id: z.string(),
210
+ authorKind: z.enum(['user', 'admin', 'author', 'system']),
211
+ author: z.string().nullable(),
212
+ audience: z.enum(['everyone', 'staff']),
213
+ body: z.string(),
214
+ createdAt: timestamp,
215
+ })
216
+
217
+ export const CommunityFeedbackSummarySchema = z.object({
218
+ id: CommunityFeedbackIdSchema,
219
+ ref: CommunityFeedbackRefSchema,
220
+ title: z.string(),
221
+ kind: CommunityFeedbackKindSchema.optional(),
222
+ target: CommunityFeedbackTargetSchema.optional(),
223
+ status: CommunityFeedbackStatusSchema,
224
+ fixedIn: version.nullable(),
225
+ affectedCount: z.number().int().nonnegative(),
226
+ source: CommunityFeedbackCreateSchema.shape.source.optional(),
227
+ confirmedCount: z.number().int().nonnegative().optional(),
228
+ createdAt: timestamp.optional(),
229
+ updatedAt: timestamp.optional(),
230
+ })
231
+ export type CommunityFeedbackSummary = z.output<typeof CommunityFeedbackSummarySchema>
232
+
233
+ /** Optional private fields are emitted only by the server's role-specific projection. */
234
+ export const CommunityFeedbackSchema = CommunityFeedbackSummarySchema.extend({
235
+ access: z.enum(['owner', 'admin', 'author', 'subscriber', 'public']),
236
+ body: z.string().optional(),
237
+ diagnostics: CommunityFeedbackDiagnosticsSchema.optional(),
238
+ excerpt: CommunityFeedbackExcerptSchema.nullable().optional(),
239
+ excerptSharedWithAuthor: z.boolean().optional(),
240
+ messages: z.array(CommunityFeedbackMessageSchema).optional(),
241
+ events: z.array(CommunityFeedbackEventSchema).optional(),
242
+ submitter: z.string().optional(),
243
+ instanceId: z.string().nullable().optional(),
244
+ priority: z.enum(['low', 'normal', 'high']).nullable().optional(),
245
+ labels: z.array(z.string()).optional(),
246
+ visibility: z.enum(['private', 'public']).optional(),
247
+ duplicateOf: CommunityFeedbackRefSchema.nullable().optional(),
248
+ githubUrl: z.string().nullable().optional(),
249
+ closedAt: timestamp.nullable().optional(),
250
+ purgedAt: timestamp.nullable().optional(),
251
+ confirmed: z.boolean().default(false),
252
+ affected: z.boolean().default(false),
253
+ affectedDiagnostics: z
254
+ .array(
255
+ z.object({
256
+ user: z.string(),
257
+ diagnostics: CommunityFeedbackDiagnosticsSchema.nullable(),
258
+ nxtVersion: version.nullable(),
259
+ createdAt: timestamp,
260
+ }),
261
+ )
262
+ .optional(),
263
+ })
264
+ export type CommunityFeedback = z.output<typeof CommunityFeedbackSchema>
265
+ export const CommunityFeedbackListSchema = z.object({
266
+ items: z.array(CommunityFeedbackSummarySchema),
267
+ cursor: z.string().nullable(),
268
+ })
269
+ export const CommunityFeedbackChangesSchema = z.object({
270
+ events: z.array(CommunityFeedbackEventSchema),
271
+ cursor: z.number().int().nonnegative(),
272
+ hasMore: z.boolean(),
273
+ })
274
+ export type CommunityFeedbackChanges = z.output<typeof CommunityFeedbackChangesSchema>
275
+ export const CommunityFeedbackDigestSchema = z.object({
276
+ since: timestamp,
277
+ counts: z.object({
278
+ status: z.record(CommunityFeedbackStatusSchema, z.number().int().nonnegative()),
279
+ kind: z.record(CommunityFeedbackKindSchema, z.number().int().nonnegative()),
280
+ target: z.record(z.enum(['nxt', 'extension']), z.number().int().nonnegative()),
281
+ extensions: z.record(z.string(), z.number().int().nonnegative()),
282
+ }),
283
+ newItems: z.array(CommunityFeedbackSummarySchema),
284
+ awaitingReply: z.array(CommunityFeedbackSummarySchema),
285
+ mostAffected: z.array(CommunityFeedbackSummarySchema),
286
+ unconfirmedFixes: z.array(CommunityFeedbackSummarySchema),
287
+ fixedCount: z.number().int().nonnegative(),
288
+ })
289
+ export type CommunityFeedbackDigest = z.output<typeof CommunityFeedbackDigestSchema>
package/src/community.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod'
2
- import { ExtensionIdSchema } from './domain.js'
2
+ import { ExtensionIdSchema, ExtensionRevisionIdSchema } from './domain.js'
3
3
 
4
4
  /**
5
5
  * NekroNXT 社区在本机的投影。Host 代理社区开放接口并把结果归一到这些结构;社区新增的字段在 Host 处丢弃,
@@ -90,6 +90,8 @@ export const CommunityExtensionSortSchema = z.enum(['updated', 'score', 'downloa
90
90
  export const CommunityReleaseIdSchema = z.string().regex(/^rel_[0-9A-Za-z]+$/u)
91
91
 
92
92
  export const CommunityAccountSchema = z.object({
93
+ /** Immutable community identity; absent when connected to a community predating feedback support. */
94
+ id: z.string().min(1).max(100).optional(),
93
95
  handle: z.string().min(1).max(80),
94
96
  displayName: z.string().max(200),
95
97
  avatarUrl: z.string().url().nullable(),
@@ -100,6 +102,7 @@ export const CommunityEnvironmentSchema = z.enum(['production', 'staging', 'deve
100
102
  export type CommunityEnvironment = z.output<typeof CommunityEnvironmentSchema>
101
103
 
102
104
  export const CommunityStatusSchema = z.object({
105
+ features: z.array(z.string()).optional(),
103
106
  communityUrl: z.string().url(),
104
107
  /** 社区自我声明的环境;连不上或旧版社区时为 null。 */
105
108
  environment: CommunityEnvironmentSchema.nullable(),
@@ -131,6 +134,12 @@ export const CommunityReleaseSchema = z.object({
131
134
  requiresSdk: z.number().int().positive().nullable(),
132
135
  notes: z.string().max(2000),
133
136
  createdAt: z.number().int(),
137
+ /** Release number on the community line; absent from older communities. */
138
+ number: z.number().int().positive().optional(),
139
+ /** Revision the release contains and its lineage, so NekroNXT can tell fast-forward from divergence. */
140
+ revisionId: ExtensionRevisionIdSchema.optional(),
141
+ parentRevisionId: ExtensionRevisionIdSchema.nullable().optional(),
142
+ ancestry: z.array(ExtensionRevisionIdSchema).max(256).optional(),
134
143
  })
135
144
  export type CommunityRelease = z.output<typeof CommunityReleaseSchema>
136
145
 
@@ -157,6 +166,20 @@ export const CommunityExtensionSummarySchema = z.object({
157
166
  downloads: z.number().int().nonnegative(),
158
167
  updatedAt: z.number().int(),
159
168
  pageUrl: z.string().url(),
169
+ /**
170
+ * The extension and version this one is a branch of; null for an original line or an older community. Without a
171
+ * displayName the origin is not public on the community.
172
+ */
173
+ branchOf: z
174
+ .object({
175
+ extensionId: ExtensionIdSchema,
176
+ revisionId: ExtensionRevisionIdSchema.nullable(),
177
+ displayName: z.string().max(200).nullable(),
178
+ })
179
+ .nullable()
180
+ .optional(),
181
+ /** Public branches made from this extension. */
182
+ branchCount: z.number().int().nonnegative().optional(),
160
183
  })
161
184
  export type CommunityExtensionSummary = z.output<typeof CommunityExtensionSummarySchema>
162
185
 
@@ -180,6 +203,13 @@ export const CommunityExtensionDetailSchema = CommunityExtensionSummarySchema.ex
180
203
  })
181
204
  export type CommunityExtensionDetail = z.output<typeof CommunityExtensionDetailSchema>
182
205
 
206
+ /**
207
+ * 本机缓存的社区目录读取附带的时间:`fetchedAt` 是这份内容从社区取回的时间;`stale` 表示社区这次没能读取,
208
+ * 返回的是较早的一份。
209
+ */
210
+ export const CommunityFreshnessSchema = z.object({ fetchedAt: z.number().int().nonnegative(), stale: z.boolean() })
211
+ export type CommunityFreshness = z.output<typeof CommunityFreshnessSchema>
212
+
183
213
  /**
184
214
  * 发布时随包提交的条目信息(作者自己写的介绍)。首次发布用于建条目;之后提供了哪项就覆盖哪项,未提供的保持不变。
185
215
  */
@@ -220,6 +250,38 @@ export const CommunitySourceSchema = z.object({
220
250
  })
221
251
  export type CommunitySource = z.output<typeof CommunitySourceSchema>
222
252
 
253
+ /**
254
+ * How this machine's current version of one extension relates to the community's newest public release, with the
255
+ * action that relation calls for. `role` says whether this machine may publish the line or only follows it.
256
+ */
257
+ export const CommunitySyncItemSchema = z.object({
258
+ extensionId: ExtensionIdSchema,
259
+ displayName: z.string(),
260
+ communityUrl: z.string().url(),
261
+ role: z.enum(['owner', 'subscriber']),
262
+ currentRevisionId: ExtensionRevisionIdSchema,
263
+ currentVersionLabel: z.string(),
264
+ /** Release the current version is, when it is one. */
265
+ currentRelease: z.object({ id: CommunityReleaseIdSchema, number: z.number().int().positive().nullable() }).nullable(),
266
+ /** `remote-ahead`: update; `local-ahead`: publish (owner) or local changes (subscriber); `diverged`: merge first. */
267
+ status: z.enum(['same', 'remote-ahead', 'local-ahead', 'diverged', 'unrelated', 'unknown']),
268
+ found: z.boolean(),
269
+ delisted: z.boolean(),
270
+ delistedReason: z.string().nullable(),
271
+ /** The release the current version came from was withdrawn by its author. */
272
+ releaseWithdrawn: z.boolean(),
273
+ latest: CommunityReleaseSchema.nullable(),
274
+ /** The user chose to keep local changes over `latest`; it is not offered again. */
275
+ dismissed: z.boolean(),
276
+ addedPermissions: z.array(CommunityPermissionItemSchema),
277
+ })
278
+ export type CommunitySyncItem = z.output<typeof CommunitySyncItemSchema>
279
+
280
+ export const CommunitySyncSchema = z.object({
281
+ checkedAt: z.number().int().nullable(),
282
+ items: z.array(CommunitySyncItemSchema),
283
+ })
284
+
223
285
  export const CommunityInstalledItemSchema = z.object({
224
286
  extensionId: ExtensionIdSchema,
225
287
  displayName: z.string(),
@@ -6,8 +6,9 @@ import { JsonValueSchema, type JsonValue } from './domain.js'
6
6
  * product form can render and the Host can validate without executing Schema callbacks.
7
7
  *
8
8
  * Supported nodes: object (dict), string, number, natural, percent, boolean, const, union of consts (enum) and
9
- * array of primitives. `meta.role: 'secret'` marks a write-only credential. NekroNXT reads three extra meta keys:
10
- * `hint` (help text), `advanced` (collapsed by default) and `visibleWhen` (show only when a sibling equals a value).
9
+ * array of primitives. `meta.role: 'secret'` marks a write-only credential. NekroNXT reads four extra meta keys:
10
+ * `hint` (help text), `advanced` (collapsed by default), `visibleWhen` (show only when a sibling equals a value) and
11
+ * `agentWritable: false` (only an administrator may change the field, in the console).
11
12
  */
12
13
  export const CONFIG_SCHEMA_MAX_BYTES = 64 * 1024
13
14
 
@@ -27,6 +28,8 @@ export const ConfigSchemaMetaSchema = z
27
28
  pattern: z.object({ source: z.string().max(500), flags: z.string().max(8).optional() }).optional(),
28
29
  advanced: z.boolean().optional(),
29
30
  group: z.string().max(40).optional(),
31
+ /** `false` keeps the field out of the agent's reach: only an administrator changes it, in the console. */
32
+ agentWritable: z.boolean().optional(),
30
33
  visibleWhen: z
31
34
  .object({ key: z.string().min(1).max(64), equals: JsonValueSchema })
32
35
  .strict()
@@ -105,9 +108,27 @@ export interface ConfigField {
105
108
  readonly advanced: boolean
106
109
  readonly group?: string
107
110
  readonly options?: readonly { readonly value: JsonValue; readonly label: string }[]
111
+ /** Declared `meta.agentWritable: false`: only an administrator changes it. */
112
+ readonly adminOnly: boolean
108
113
  readonly node: ConfigSchemaNode
109
114
  }
110
115
 
116
+ /**
117
+ * How much a change to one field matters when an agent makes it: `admin` fields are changed in the console only,
118
+ * `secret` fields hold credentials, `network` fields decide where the extension may connect, the rest are `normal`.
119
+ */
120
+ export type ConfigFieldLevel = 'normal' | 'network' | 'secret' | 'admin'
121
+
122
+ export const configFieldLevel = (
123
+ field: Pick<ConfigField, 'key' | 'kind' | 'adminOnly'>,
124
+ networkFields: ReadonlySet<string> | readonly string[],
125
+ ): ConfigFieldLevel => {
126
+ if (field.adminOnly) return 'admin'
127
+ if (field.kind === 'secret') return 'secret'
128
+ const network = networkFields instanceof Set ? networkFields : new Set(networkFields)
129
+ return network.has(field.key) ? 'network' : 'normal'
130
+ }
131
+
111
132
  export const configDescription = (meta: ConfigSchemaMeta | undefined): string | undefined => {
112
133
  const description = meta?.description
113
134
  if (typeof description === 'string') return description
@@ -155,10 +176,17 @@ export const configFields = (document: ConfigSchemaDocument): readonly ConfigFie
155
176
  advanced: node.meta?.advanced === true,
156
177
  ...(node.meta?.group === undefined ? {} : { group: node.meta.group }),
157
178
  ...(options === undefined ? {} : { options }),
179
+ adminOnly: node.meta?.agentWritable === false,
158
180
  node,
159
181
  }
160
182
  })
161
183
 
184
+ /** Fields declared `meta.agentWritable: false`. */
185
+ export const adminOnlyConfigKeys = (document: ConfigSchemaDocument): readonly string[] =>
186
+ configFields(document)
187
+ .filter((field) => field.adminOnly)
188
+ .map((field) => field.key)
189
+
162
190
  export const configSecretKeys = (document: ConfigSchemaDocument): readonly string[] =>
163
191
  configFields(document)
164
192
  .filter((field) => field.kind === 'secret')
@@ -302,6 +330,8 @@ interface FieldOptions<Value extends JsonValue> {
302
330
  readonly required?: boolean
303
331
  readonly advanced?: boolean
304
332
  readonly group?: string
333
+ /** `false` for billing or moderation switches only an administrator should change. */
334
+ readonly agentWritable?: boolean
305
335
  }
306
336
 
307
337
  const fieldMeta = <Value extends JsonValue>(title: string, options: FieldOptions<Value> = {}): ConfigSchemaMeta => ({
@@ -311,6 +341,7 @@ const fieldMeta = <Value extends JsonValue>(title: string, options: FieldOptions
311
341
  ...(options.required === undefined ? {} : { required: options.required }),
312
342
  ...(options.advanced === undefined ? {} : { advanced: options.advanced }),
313
343
  ...(options.group === undefined ? {} : { group: options.group }),
344
+ ...(options.agentWritable === undefined ? {} : { agentWritable: options.agentWritable }),
314
345
  })
315
346
 
316
347
  /** Builds serialized Schemastery documents without depending on the Schemastery runtime. */