@dobby.ai/dobby 0.1.1 → 0.1.2

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 (136) hide show
  1. package/README.md +20 -7
  2. package/dist/src/agent/event-forwarder.js +185 -16
  3. package/dist/src/cli/commands/cron.js +39 -35
  4. package/dist/src/cli/program.js +0 -6
  5. package/dist/src/core/types.js +2 -0
  6. package/dist/src/cron/config.js +2 -2
  7. package/dist/src/cron/service.js +87 -23
  8. package/dist/src/cron/store.js +1 -1
  9. package/package.json +9 -3
  10. package/.env.example +0 -8
  11. package/AGENTS.md +0 -267
  12. package/ROADMAP.md +0 -34
  13. package/config/cron.example.json +0 -9
  14. package/config/gateway.example.json +0 -132
  15. package/dist/plugins/connector-discord/src/mapper.js +0 -75
  16. package/dist/src/cli/tests/config-command.test.js +0 -42
  17. package/dist/src/cli/tests/config-io.test.js +0 -64
  18. package/dist/src/cli/tests/config-mutators.test.js +0 -47
  19. package/dist/src/cli/tests/discord-mapper.test.js +0 -90
  20. package/dist/src/cli/tests/doctor.test.js +0 -252
  21. package/dist/src/cli/tests/init-catalog.test.js +0 -134
  22. package/dist/src/cli/tests/program-options.test.js +0 -78
  23. package/dist/src/cli/tests/routing-config.test.js +0 -254
  24. package/dist/src/core/tests/control-command.test.js +0 -17
  25. package/dist/src/core/tests/runtime-registry.test.js +0 -116
  26. package/dist/src/core/tests/typing-controller.test.js +0 -103
  27. package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
  28. package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
  29. package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
  30. package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
  31. package/docs/MVP.md +0 -135
  32. package/docs/RUNBOOK.md +0 -243
  33. package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
  34. package/plugins/connector-discord/dobby.manifest.json +0 -18
  35. package/plugins/connector-discord/index.js +0 -1
  36. package/plugins/connector-discord/package-lock.json +0 -360
  37. package/plugins/connector-discord/package.json +0 -38
  38. package/plugins/connector-discord/src/connector.ts +0 -345
  39. package/plugins/connector-discord/src/contribution.ts +0 -21
  40. package/plugins/connector-discord/src/mapper.ts +0 -101
  41. package/plugins/connector-discord/tsconfig.json +0 -19
  42. package/plugins/connector-feishu/dobby.manifest.json +0 -18
  43. package/plugins/connector-feishu/index.js +0 -1
  44. package/plugins/connector-feishu/package-lock.json +0 -618
  45. package/plugins/connector-feishu/package.json +0 -38
  46. package/plugins/connector-feishu/src/connector.ts +0 -343
  47. package/plugins/connector-feishu/src/contribution.ts +0 -26
  48. package/plugins/connector-feishu/src/mapper.ts +0 -401
  49. package/plugins/connector-feishu/tsconfig.json +0 -19
  50. package/plugins/plugin-sdk/index.d.ts +0 -261
  51. package/plugins/plugin-sdk/index.js +0 -1
  52. package/plugins/plugin-sdk/package-lock.json +0 -12
  53. package/plugins/plugin-sdk/package.json +0 -22
  54. package/plugins/provider-claude/dobby.manifest.json +0 -17
  55. package/plugins/provider-claude/index.js +0 -1
  56. package/plugins/provider-claude/package-lock.json +0 -3398
  57. package/plugins/provider-claude/package.json +0 -39
  58. package/plugins/provider-claude/src/contribution.ts +0 -1018
  59. package/plugins/provider-claude/tsconfig.json +0 -19
  60. package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
  61. package/plugins/provider-claude-cli/index.js +0 -1
  62. package/plugins/provider-claude-cli/package-lock.json +0 -2898
  63. package/plugins/provider-claude-cli/package.json +0 -38
  64. package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
  65. package/plugins/provider-claude-cli/tsconfig.json +0 -19
  66. package/plugins/provider-pi/dobby.manifest.json +0 -17
  67. package/plugins/provider-pi/index.js +0 -1
  68. package/plugins/provider-pi/package-lock.json +0 -3877
  69. package/plugins/provider-pi/package.json +0 -40
  70. package/plugins/provider-pi/src/contribution.ts +0 -606
  71. package/plugins/provider-pi/tsconfig.json +0 -19
  72. package/plugins/sandbox-core/boxlite.js +0 -1
  73. package/plugins/sandbox-core/dobby.manifest.json +0 -17
  74. package/plugins/sandbox-core/docker.js +0 -1
  75. package/plugins/sandbox-core/package-lock.json +0 -136
  76. package/plugins/sandbox-core/package.json +0 -39
  77. package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
  78. package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
  79. package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
  80. package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
  81. package/plugins/sandbox-core/src/docker-executor.ts +0 -217
  82. package/plugins/sandbox-core/tsconfig.json +0 -19
  83. package/scripts/local-extensions.mjs +0 -168
  84. package/src/agent/event-forwarder.ts +0 -414
  85. package/src/cli/commands/config.ts +0 -328
  86. package/src/cli/commands/configure.ts +0 -92
  87. package/src/cli/commands/cron.ts +0 -410
  88. package/src/cli/commands/doctor.ts +0 -331
  89. package/src/cli/commands/extension.ts +0 -207
  90. package/src/cli/commands/init.ts +0 -211
  91. package/src/cli/commands/start.ts +0 -223
  92. package/src/cli/commands/topology.ts +0 -415
  93. package/src/cli/index.ts +0 -9
  94. package/src/cli/program.ts +0 -314
  95. package/src/cli/shared/config-io.ts +0 -245
  96. package/src/cli/shared/config-mutators.ts +0 -470
  97. package/src/cli/shared/config-schema.ts +0 -228
  98. package/src/cli/shared/config-types.ts +0 -129
  99. package/src/cli/shared/configure-sections.ts +0 -595
  100. package/src/cli/shared/discord-config.ts +0 -14
  101. package/src/cli/shared/init-catalog.ts +0 -249
  102. package/src/cli/shared/local-extension-specs.ts +0 -108
  103. package/src/cli/shared/runtime.ts +0 -33
  104. package/src/cli/shared/schema-prompts.ts +0 -443
  105. package/src/cli/tests/config-command.test.ts +0 -56
  106. package/src/cli/tests/config-io.test.ts +0 -92
  107. package/src/cli/tests/config-mutators.test.ts +0 -59
  108. package/src/cli/tests/discord-mapper.test.ts +0 -128
  109. package/src/cli/tests/doctor.test.ts +0 -269
  110. package/src/cli/tests/init-catalog.test.ts +0 -144
  111. package/src/cli/tests/program-options.test.ts +0 -95
  112. package/src/cli/tests/routing-config.test.ts +0 -281
  113. package/src/core/control-command.ts +0 -12
  114. package/src/core/dedup-store.ts +0 -103
  115. package/src/core/gateway.ts +0 -609
  116. package/src/core/routing.ts +0 -404
  117. package/src/core/runtime-registry.ts +0 -141
  118. package/src/core/tests/control-command.test.ts +0 -20
  119. package/src/core/tests/runtime-registry.test.ts +0 -140
  120. package/src/core/tests/typing-controller.test.ts +0 -129
  121. package/src/core/types.ts +0 -324
  122. package/src/core/typing-controller.ts +0 -119
  123. package/src/cron/config.ts +0 -154
  124. package/src/cron/schedule.ts +0 -61
  125. package/src/cron/service.ts +0 -249
  126. package/src/cron/store.ts +0 -155
  127. package/src/cron/types.ts +0 -60
  128. package/src/extension/loader.ts +0 -145
  129. package/src/extension/manager.ts +0 -355
  130. package/src/extension/manifest.ts +0 -26
  131. package/src/extension/registry.ts +0 -229
  132. package/src/main.ts +0 -8
  133. package/src/sandbox/executor.ts +0 -44
  134. package/src/sandbox/host-executor.ts +0 -118
  135. package/src/shared/dobby-repo.ts +0 -48
  136. package/tsconfig.json +0 -18
@@ -1,470 +0,0 @@
1
- import type { ExtensionContributionManifest } from "../../core/types.js";
2
- import type {
3
- ContributionInstanceTemplate,
4
- ContributionTemplatesByKind,
5
- NormalizedGatewayConfig,
6
- RawBindingConfig,
7
- RawDefaultBindingConfig,
8
- RawExtensionItemConfig,
9
- RawGatewayConfig,
10
- RawRouteDefaults,
11
- RawRouteProfile,
12
- } from "./config-types.js";
13
-
14
- const DEFAULT_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
15
-
16
- function isRecord(value: unknown): value is Record<string, unknown> {
17
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
18
- }
19
-
20
- function asItemMap(value: unknown): Record<string, RawExtensionItemConfig> {
21
- if (!isRecord(value)) {
22
- return {};
23
- }
24
-
25
- const result: Record<string, RawExtensionItemConfig> = {};
26
- for (const [instanceId, raw] of Object.entries(value)) {
27
- if (!isRecord(raw) || typeof raw.type !== "string" || raw.type.trim().length === 0) {
28
- continue;
29
- }
30
-
31
- result[instanceId] = {
32
- ...raw,
33
- type: raw.type,
34
- };
35
- }
36
-
37
- return result;
38
- }
39
-
40
- function asAllowList(value: unknown): Array<{ package: string; enabled: boolean }> {
41
- if (!Array.isArray(value)) {
42
- return [];
43
- }
44
-
45
- const normalized: Array<{ package: string; enabled: boolean }> = [];
46
- for (const item of value) {
47
- if (!isRecord(item) || typeof item.package !== "string" || item.package.trim().length === 0) {
48
- continue;
49
- }
50
-
51
- normalized.push({
52
- package: item.package,
53
- enabled: item.enabled !== false,
54
- });
55
- }
56
-
57
- return normalized;
58
- }
59
-
60
- function asRouteDefaults(value: unknown): RawRouteDefaults {
61
- if (!isRecord(value)) {
62
- return {
63
- tools: "full",
64
- mentions: "required",
65
- };
66
- }
67
-
68
- return {
69
- ...(typeof value.projectRoot === "string" && value.projectRoot.trim().length > 0 ? { projectRoot: value.projectRoot } : {}),
70
- ...(typeof value.provider === "string" && value.provider.trim().length > 0 ? { provider: value.provider } : {}),
71
- ...(typeof value.sandbox === "string" && value.sandbox.trim().length > 0 ? { sandbox: value.sandbox } : {}),
72
- tools: value.tools === "readonly" ? "readonly" : "full",
73
- mentions: value.mentions === "optional" ? "optional" : "required",
74
- };
75
- }
76
-
77
- function asRoutes(value: unknown): Record<string, RawRouteProfile> {
78
- if (!isRecord(value)) {
79
- return {};
80
- }
81
-
82
- const normalized: Record<string, RawRouteProfile> = {};
83
- for (const [routeId, route] of Object.entries(value)) {
84
- if (!isRecord(route)) {
85
- continue;
86
- }
87
-
88
- normalized[routeId] = {
89
- ...route,
90
- ...(typeof route.projectRoot === "string" && route.projectRoot.trim().length > 0 ? { projectRoot: route.projectRoot } : {}),
91
- ...(route.tools === "readonly" ? { tools: "readonly" as const } : {}),
92
- ...(route.mentions === "optional" ? { mentions: "optional" as const } : {}),
93
- ...(typeof route.provider === "string" && route.provider.trim().length > 0 ? { provider: route.provider } : {}),
94
- ...(typeof route.sandbox === "string" && route.sandbox.trim().length > 0 ? { sandbox: route.sandbox } : {}),
95
- ...(typeof route.systemPromptFile === "string" ? { systemPromptFile: route.systemPromptFile } : {}),
96
- };
97
- }
98
-
99
- return normalized;
100
- }
101
-
102
- function asDefaultBinding(value: unknown): RawDefaultBindingConfig | undefined {
103
- if (!isRecord(value) || typeof value.route !== "string" || value.route.trim().length === 0) {
104
- return undefined;
105
- }
106
-
107
- return {
108
- ...value,
109
- route: value.route,
110
- };
111
- }
112
-
113
- function asBindings(value: unknown): Record<string, RawBindingConfig> {
114
- if (!isRecord(value)) {
115
- return {};
116
- }
117
-
118
- const normalized: Record<string, RawBindingConfig> = {};
119
- for (const [bindingId, binding] of Object.entries(value)) {
120
- if (!isRecord(binding)) {
121
- continue;
122
- }
123
-
124
- const rawSource = binding.source;
125
- if (
126
- typeof binding.connector !== "string"
127
- || binding.connector.trim().length === 0
128
- || typeof binding.route !== "string"
129
- || binding.route.trim().length === 0
130
- || !isRecord(rawSource)
131
- || (rawSource.type !== "channel" && rawSource.type !== "chat")
132
- || typeof rawSource.id !== "string"
133
- || rawSource.id.trim().length === 0
134
- ) {
135
- continue;
136
- }
137
-
138
- normalized[bindingId] = {
139
- ...binding,
140
- connector: binding.connector,
141
- route: binding.route,
142
- source: {
143
- ...rawSource,
144
- type: rawSource.type,
145
- id: rawSource.id,
146
- },
147
- };
148
- }
149
-
150
- return normalized;
151
- }
152
-
153
- export function ensureGatewayConfigShape(config: RawGatewayConfig): NormalizedGatewayConfig {
154
- const normalizedProvidersDefault =
155
- typeof config.providers?.default === "string" && config.providers.default.trim().length > 0
156
- ? config.providers.default
157
- : "";
158
- const normalizedSandboxesDefault =
159
- typeof config.sandboxes?.default === "string" && config.sandboxes.default.trim().length > 0
160
- ? config.sandboxes.default
161
- : "host.builtin";
162
-
163
- const routeDefaults = asRouteDefaults(config.routes?.default);
164
- if (!routeDefaults.provider && normalizedProvidersDefault) {
165
- routeDefaults.provider = normalizedProvidersDefault;
166
- }
167
- if (!routeDefaults.sandbox && normalizedSandboxesDefault) {
168
- routeDefaults.sandbox = normalizedSandboxesDefault;
169
- }
170
-
171
- const defaultBinding = asDefaultBinding(config.bindings?.default);
172
-
173
- return {
174
- ...config,
175
- extensions: {
176
- ...((isRecord(config.extensions) ? config.extensions : {}) as Record<string, unknown>),
177
- allowList: asAllowList(config.extensions?.allowList),
178
- },
179
- providers: {
180
- ...((isRecord(config.providers) ? config.providers : {}) as Record<string, unknown>),
181
- default: normalizedProvidersDefault,
182
- items: asItemMap(config.providers?.items),
183
- },
184
- connectors: {
185
- ...((isRecord(config.connectors) ? config.connectors : {}) as Record<string, unknown>),
186
- items: asItemMap(config.connectors?.items),
187
- },
188
- sandboxes: {
189
- ...((isRecord(config.sandboxes) ? config.sandboxes : {}) as Record<string, unknown>),
190
- default: normalizedSandboxesDefault,
191
- items: asItemMap(config.sandboxes?.items),
192
- },
193
- routes: {
194
- ...((isRecord(config.routes) ? config.routes : {}) as Record<string, unknown>),
195
- default: routeDefaults,
196
- items: asRoutes(config.routes?.items),
197
- },
198
- bindings: {
199
- ...((isRecord(config.bindings) ? config.bindings : {}) as Record<string, unknown>),
200
- ...(defaultBinding ? { default: defaultBinding } : {}),
201
- items: asBindings(config.bindings?.items),
202
- },
203
- data: {
204
- ...((isRecord(config.data) ? config.data : {}) as Record<string, unknown>),
205
- rootDir: typeof config.data?.rootDir === "string" && config.data.rootDir.trim().length > 0 ? config.data.rootDir : "./data",
206
- dedupTtlMs:
207
- typeof config.data?.dedupTtlMs === "number" && Number.isFinite(config.data.dedupTtlMs) && config.data.dedupTtlMs > 0
208
- ? config.data.dedupTtlMs
209
- : DEFAULT_DEDUP_TTL_MS,
210
- },
211
- };
212
- }
213
-
214
- export function upsertAllowListPackage(config: RawGatewayConfig, packageName: string, enabled = true): void {
215
- const next = ensureGatewayConfigShape(config);
216
- const allowList = next.extensions.allowList;
217
- const existing = allowList.find((item) => item.package === packageName);
218
- if (existing) {
219
- existing.enabled = enabled;
220
- config.extensions = next.extensions;
221
- return;
222
- }
223
-
224
- allowList.push({ package: packageName, enabled });
225
- config.extensions = {
226
- ...next.extensions,
227
- allowList,
228
- };
229
- }
230
-
231
- function buildTemplateInstanceId(contributionId: string): string {
232
- const segments = contributionId.split(".");
233
- const suffix = segments.length > 1 ? segments.slice(1).join("-") : contributionId;
234
- return `${suffix}.main`;
235
- }
236
-
237
- export function buildContributionTemplates(contributions: ExtensionContributionManifest[]): ContributionTemplatesByKind {
238
- const templates: ContributionTemplatesByKind = {
239
- providers: [],
240
- connectors: [],
241
- sandboxes: [],
242
- };
243
-
244
- for (const contribution of contributions) {
245
- const template: ContributionInstanceTemplate = {
246
- id: buildTemplateInstanceId(contribution.id),
247
- type: contribution.id,
248
- config: {},
249
- };
250
-
251
- if (contribution.kind === "provider") {
252
- templates.providers.push(template);
253
- continue;
254
- }
255
-
256
- if (contribution.kind === "connector") {
257
- templates.connectors.push(template);
258
- continue;
259
- }
260
-
261
- templates.sandboxes.push(template);
262
- }
263
-
264
- return templates;
265
- }
266
-
267
- function upsertTemplateInstances(
268
- items: Record<string, RawExtensionItemConfig>,
269
- templates: ContributionInstanceTemplate[],
270
- ): string[] {
271
- const byType = new Set(Object.values(items).map((instance) => instance.type));
272
- const addedIds: string[] = [];
273
-
274
- for (const template of templates) {
275
- if (byType.has(template.type)) {
276
- continue;
277
- }
278
-
279
- let candidateId = template.id;
280
- let suffix = 2;
281
- while (items[candidateId]) {
282
- candidateId = `${template.id}-${suffix}`;
283
- suffix += 1;
284
- }
285
-
286
- items[candidateId] = {
287
- type: template.type,
288
- ...structuredClone(template.config),
289
- };
290
- byType.add(template.type);
291
- addedIds.push(candidateId);
292
- }
293
-
294
- return addedIds;
295
- }
296
-
297
- export function applyContributionTemplates(config: RawGatewayConfig, templates: ContributionTemplatesByKind): {
298
- providers: string[];
299
- connectors: string[];
300
- sandboxes: string[];
301
- } {
302
- const next = ensureGatewayConfigShape(config);
303
- const providerItems = next.providers.items;
304
- const connectorItems = next.connectors.items;
305
- const sandboxItems = next.sandboxes.items;
306
-
307
- const added = {
308
- providers: upsertTemplateInstances(providerItems, templates.providers),
309
- connectors: upsertTemplateInstances(connectorItems, templates.connectors),
310
- sandboxes: upsertTemplateInstances(sandboxItems, templates.sandboxes),
311
- };
312
-
313
- config.providers = {
314
- ...next.providers,
315
- items: providerItems,
316
- };
317
- config.connectors = {
318
- ...next.connectors,
319
- items: connectorItems,
320
- };
321
- config.sandboxes = {
322
- ...next.sandboxes,
323
- items: sandboxItems,
324
- };
325
-
326
- return added;
327
- }
328
-
329
- export function upsertProviderInstance(
330
- config: RawGatewayConfig,
331
- instanceId: string,
332
- type: string,
333
- instanceConfig: Record<string, unknown>,
334
- ): void {
335
- const next = ensureGatewayConfigShape(config);
336
- next.providers.items[instanceId] = {
337
- type,
338
- ...structuredClone(instanceConfig),
339
- };
340
- config.providers = {
341
- ...next.providers,
342
- items: next.providers.items,
343
- };
344
- }
345
-
346
- export function upsertConnectorInstance(
347
- config: RawGatewayConfig,
348
- instanceId: string,
349
- type: string,
350
- instanceConfig: Record<string, unknown>,
351
- ): void {
352
- const next = ensureGatewayConfigShape(config);
353
- next.connectors.items[instanceId] = {
354
- type,
355
- ...structuredClone(instanceConfig),
356
- };
357
- config.connectors = {
358
- ...next.connectors,
359
- items: next.connectors.items,
360
- };
361
- }
362
-
363
- export function upsertSandboxInstance(
364
- config: RawGatewayConfig,
365
- instanceId: string,
366
- type: string,
367
- instanceConfig: Record<string, unknown>,
368
- ): void {
369
- const next = ensureGatewayConfigShape(config);
370
- next.sandboxes.items[instanceId] = {
371
- type,
372
- ...structuredClone(instanceConfig),
373
- };
374
- config.sandboxes = {
375
- ...next.sandboxes,
376
- items: next.sandboxes.items,
377
- };
378
- }
379
-
380
- export function setDefaultProviderIfMissingOrInvalid(config: RawGatewayConfig): void {
381
- const next = ensureGatewayConfigShape(config);
382
- const items = next.providers.items;
383
- const defaultProvider = next.providers.default;
384
-
385
- if (defaultProvider && items[defaultProvider]) {
386
- config.providers = next.providers;
387
- if (!next.routes.default.provider) {
388
- config.routes = {
389
- ...next.routes,
390
- defaults: {
391
- ...next.routes.default,
392
- provider: defaultProvider,
393
- },
394
- };
395
- }
396
- return;
397
- }
398
-
399
- const candidates = Object.keys(items).sort((a, b) => a.localeCompare(b));
400
- if (candidates.length === 0) {
401
- config.providers = next.providers;
402
- return;
403
- }
404
-
405
- config.providers = {
406
- ...next.providers,
407
- default: candidates[0]!,
408
- items,
409
- };
410
- config.routes = {
411
- ...next.routes,
412
- default: {
413
- ...next.routes.default,
414
- provider: candidates[0]!,
415
- },
416
- };
417
- }
418
-
419
- export function upsertRoute(config: RawGatewayConfig, routeId: string, profile: RawRouteProfile): void {
420
- const next = ensureGatewayConfigShape(config);
421
- next.routes.items[routeId] = {
422
- ...(typeof profile.projectRoot === "string" && profile.projectRoot.trim().length > 0 ? { projectRoot: profile.projectRoot } : {}),
423
- ...(profile.tools ? { tools: profile.tools } : {}),
424
- ...(profile.mentions ? { mentions: profile.mentions } : {}),
425
- ...(profile.provider ? { provider: profile.provider } : {}),
426
- ...(profile.sandbox ? { sandbox: profile.sandbox } : {}),
427
- ...(typeof profile.systemPromptFile === "string" ? { systemPromptFile: profile.systemPromptFile } : {}),
428
- };
429
- config.routes = {
430
- ...next.routes,
431
- items: next.routes.items,
432
- };
433
- }
434
-
435
- export function upsertBinding(config: RawGatewayConfig, bindingId: string, binding: RawBindingConfig): void {
436
- const next = ensureGatewayConfigShape(config);
437
- next.bindings.items[bindingId] = structuredClone(binding);
438
- config.bindings = {
439
- ...next.bindings,
440
- items: next.bindings.items,
441
- };
442
- }
443
-
444
- export function setDefaultBinding(config: RawGatewayConfig, binding: RawDefaultBindingConfig | undefined): void {
445
- const next = ensureGatewayConfigShape(config);
446
- const normalizedBinding = binding ? structuredClone(binding) : undefined;
447
- config.bindings = {
448
- ...next.bindings,
449
- ...(normalizedBinding ? { default: normalizedBinding } : {}),
450
- items: next.bindings.items,
451
- };
452
-
453
- if (!normalizedBinding) {
454
- delete config.bindings.default;
455
- }
456
- }
457
-
458
- export function listContributionIds(config: RawGatewayConfig): {
459
- providers: string[];
460
- connectors: string[];
461
- sandboxes: string[];
462
- } {
463
- const next = ensureGatewayConfigShape(config);
464
-
465
- return {
466
- providers: Object.values(next.providers.items).map((instance) => instance.type),
467
- connectors: Object.values(next.connectors.items).map((instance) => instance.type),
468
- sandboxes: Object.values(next.sandboxes.items).map((instance) => instance.type),
469
- };
470
- }
@@ -1,228 +0,0 @@
1
- import { Ajv, type ErrorObject, type ValidateFunction } from "ajv";
2
- import { join } from "node:path";
3
- import pino from "pino";
4
- import type { ExtensionKind } from "../../core/types.js";
5
- import { ExtensionLoader } from "../../extension/loader.js";
6
- import { ExtensionRegistry } from "../../extension/registry.js";
7
- import { ensureGatewayConfigShape } from "./config-mutators.js";
8
- import { resolveDataRootDir } from "./config-io.js";
9
- import type { RawExtensionItemConfig, RawGatewayConfig } from "./config-types.js";
10
-
11
- export interface ContributionSchemaCatalogEntry {
12
- contributionId: string;
13
- packageName: string;
14
- kind: ExtensionKind;
15
- configSchema?: Record<string, unknown>;
16
- }
17
-
18
- export interface ContributionSchemaListItem {
19
- contributionId: string;
20
- packageName: string;
21
- kind: ExtensionKind;
22
- hasSchema: boolean;
23
- }
24
-
25
- interface InstanceValidationTask {
26
- section: "providers" | "connectors" | "sandboxes";
27
- instanceId: string;
28
- instance: RawExtensionItemConfig;
29
- }
30
-
31
- function isRecord(value: unknown): value is Record<string, unknown> {
32
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
33
- }
34
-
35
- function decodeJsonPointerSegment(value: string): string {
36
- return value.replaceAll("~1", "/").replaceAll("~0", "~");
37
- }
38
-
39
- function normalizeSchemaForValidation(schema: Record<string, unknown>): Record<string, unknown> {
40
- const cloned = structuredClone(schema);
41
- if (isRecord(cloned) && typeof cloned.$schema === "string") {
42
- delete cloned.$schema;
43
- }
44
- return cloned;
45
- }
46
-
47
- function formatErrorPath(instancePath: string): string {
48
- if (!instancePath) {
49
- return "";
50
- }
51
-
52
- const segments = instancePath
53
- .split("/")
54
- .slice(1)
55
- .map((segment) => decodeJsonPointerSegment(segment))
56
- .filter((segment) => segment.length > 0);
57
-
58
- if (segments.length === 0) {
59
- return "";
60
- }
61
-
62
- let formatted = "";
63
- for (const segment of segments) {
64
- if (/^\d+$/.test(segment)) {
65
- formatted += `[${segment}]`;
66
- continue;
67
- }
68
-
69
- if (/^[a-zA-Z_$][\w$]*$/.test(segment)) {
70
- formatted += `.${segment}`;
71
- continue;
72
- }
73
-
74
- formatted += `['${segment.replaceAll("'", "\\'")}']`;
75
- }
76
-
77
- return formatted;
78
- }
79
-
80
- function buildValidationErrorMessage(
81
- task: InstanceValidationTask,
82
- contributionId: string,
83
- errors: ErrorObject[] | null | undefined,
84
- ): string {
85
- const details = (errors ?? [])
86
- .slice(0, 5)
87
- .map((error) => {
88
- const suffix = formatErrorPath(error.instancePath);
89
- return `${task.section}.items['${task.instanceId}']${suffix}: ${error.message ?? "invalid"}`;
90
- })
91
- .join("; ");
92
-
93
- return (
94
- `Invalid config for instance '${task.instanceId}' (contribution '${contributionId}'). `
95
- + (details.length > 0 ? details : "Schema validation failed.")
96
- );
97
- }
98
-
99
- async function loadRegistryForConfig(
100
- configPath: string,
101
- rawConfig: RawGatewayConfig,
102
- ): Promise<ExtensionRegistry> {
103
- const normalized = ensureGatewayConfigShape(structuredClone(rawConfig));
104
- const rootDir = resolveDataRootDir(configPath, normalized);
105
- const loader = new ExtensionLoader(pino({ name: "dobby.config-schema", level: "silent" }), {
106
- extensionsDir: join(rootDir, "extensions"),
107
- });
108
- const loadedPackages = await loader.loadAllowList(normalized.extensions.allowList);
109
- const registry = new ExtensionRegistry();
110
- registry.registerPackages(loadedPackages);
111
- return registry;
112
- }
113
-
114
- /**
115
- * Loads contribution-level JSON Schema catalog from installed/allow-listed extensions.
116
- */
117
- export async function loadContributionSchemaCatalog(
118
- configPath: string,
119
- rawConfig: RawGatewayConfig,
120
- ): Promise<ContributionSchemaCatalogEntry[]> {
121
- const registry = await loadRegistryForConfig(configPath, rawConfig);
122
- return registry.listContributionSchemas();
123
- }
124
-
125
- /**
126
- * Lists available contribution schemas with lightweight flags for CLI display.
127
- */
128
- export async function listContributionSchemas(
129
- configPath: string,
130
- rawConfig: RawGatewayConfig,
131
- ): Promise<ContributionSchemaListItem[]> {
132
- const catalog = await loadContributionSchemaCatalog(configPath, rawConfig);
133
- return catalog.map((item) => ({
134
- contributionId: item.contributionId,
135
- packageName: item.packageName,
136
- kind: item.kind,
137
- hasSchema: Boolean(item.configSchema),
138
- }));
139
- }
140
-
141
- /**
142
- * Returns one contribution schema entry, or null when not found.
143
- */
144
- export async function getContributionSchema(
145
- configPath: string,
146
- rawConfig: RawGatewayConfig,
147
- contributionId: string,
148
- ): Promise<ContributionSchemaCatalogEntry | null> {
149
- const catalog = await loadContributionSchemaCatalog(configPath, rawConfig);
150
- return catalog.find((item) => item.contributionId === contributionId) ?? null;
151
- }
152
-
153
- /**
154
- * Applies extension config defaults and validates provider/connector/sandbox instance configs with Ajv.
155
- */
156
- export async function applyAndValidateContributionSchemas(
157
- configPath: string,
158
- rawConfig: RawGatewayConfig,
159
- ): Promise<RawGatewayConfig> {
160
- const next = ensureGatewayConfigShape(structuredClone(rawConfig));
161
- const catalog = await loadContributionSchemaCatalog(configPath, next);
162
-
163
- const schemaByContribution = new Map<string, Record<string, unknown>>();
164
- for (const entry of catalog) {
165
- if (!entry.configSchema) {
166
- continue;
167
- }
168
- schemaByContribution.set(entry.contributionId, normalizeSchemaForValidation(entry.configSchema));
169
- }
170
-
171
- const ajv = new Ajv({
172
- allErrors: true,
173
- strict: false,
174
- useDefaults: true,
175
- });
176
- const validators = new Map<string, ValidateFunction>();
177
-
178
- const tasks: InstanceValidationTask[] = [
179
- ...Object.entries(next.providers.items).map(([instanceId, instance]) => ({
180
- section: "providers" as const,
181
- instanceId,
182
- instance,
183
- })),
184
- ...Object.entries(next.connectors.items).map(([instanceId, instance]) => ({
185
- section: "connectors" as const,
186
- instanceId,
187
- instance,
188
- })),
189
- ...Object.entries(next.sandboxes.items).map(([instanceId, instance]) => ({
190
- section: "sandboxes" as const,
191
- instanceId,
192
- instance,
193
- })),
194
- ];
195
-
196
- for (const task of tasks) {
197
- const contributionId = task.instance.type;
198
- const schema = schemaByContribution.get(contributionId);
199
- if (!schema) {
200
- continue;
201
- }
202
-
203
- let validate = validators.get(contributionId);
204
- if (!validate) {
205
- const compiled = ajv.compile(schema) as ValidateFunction;
206
- validators.set(contributionId, compiled);
207
- validate = compiled;
208
- }
209
-
210
- const { type: _type, ...instanceConfig } = task.instance;
211
- const valid = validate(instanceConfig);
212
- if (!valid) {
213
- throw new Error(buildValidationErrorMessage(task, contributionId, validate.errors));
214
- }
215
-
216
- for (const key of Object.keys(task.instance)) {
217
- if (key !== "type") {
218
- delete task.instance[key];
219
- }
220
- }
221
- Object.assign(task.instance, {
222
- type: contributionId,
223
- ...instanceConfig,
224
- });
225
- }
226
-
227
- return next;
228
- }