@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.
- package/README.md +20 -7
- package/dist/src/agent/event-forwarder.js +185 -16
- package/dist/src/cli/commands/cron.js +39 -35
- package/dist/src/cli/program.js +0 -6
- package/dist/src/core/types.js +2 -0
- package/dist/src/cron/config.js +2 -2
- package/dist/src/cron/service.js +87 -23
- package/dist/src/cron/store.js +1 -1
- package/package.json +9 -3
- package/.env.example +0 -8
- package/AGENTS.md +0 -267
- package/ROADMAP.md +0 -34
- package/config/cron.example.json +0 -9
- package/config/gateway.example.json +0 -132
- package/dist/plugins/connector-discord/src/mapper.js +0 -75
- package/dist/src/cli/tests/config-command.test.js +0 -42
- package/dist/src/cli/tests/config-io.test.js +0 -64
- package/dist/src/cli/tests/config-mutators.test.js +0 -47
- package/dist/src/cli/tests/discord-mapper.test.js +0 -90
- package/dist/src/cli/tests/doctor.test.js +0 -252
- package/dist/src/cli/tests/init-catalog.test.js +0 -134
- package/dist/src/cli/tests/program-options.test.js +0 -78
- package/dist/src/cli/tests/routing-config.test.js +0 -254
- package/dist/src/core/tests/control-command.test.js +0 -17
- package/dist/src/core/tests/runtime-registry.test.js +0 -116
- package/dist/src/core/tests/typing-controller.test.js +0 -103
- package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
- package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
- package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
- package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
- package/docs/MVP.md +0 -135
- package/docs/RUNBOOK.md +0 -243
- package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
- package/plugins/connector-discord/dobby.manifest.json +0 -18
- package/plugins/connector-discord/index.js +0 -1
- package/plugins/connector-discord/package-lock.json +0 -360
- package/plugins/connector-discord/package.json +0 -38
- package/plugins/connector-discord/src/connector.ts +0 -345
- package/plugins/connector-discord/src/contribution.ts +0 -21
- package/plugins/connector-discord/src/mapper.ts +0 -101
- package/plugins/connector-discord/tsconfig.json +0 -19
- package/plugins/connector-feishu/dobby.manifest.json +0 -18
- package/plugins/connector-feishu/index.js +0 -1
- package/plugins/connector-feishu/package-lock.json +0 -618
- package/plugins/connector-feishu/package.json +0 -38
- package/plugins/connector-feishu/src/connector.ts +0 -343
- package/plugins/connector-feishu/src/contribution.ts +0 -26
- package/plugins/connector-feishu/src/mapper.ts +0 -401
- package/plugins/connector-feishu/tsconfig.json +0 -19
- package/plugins/plugin-sdk/index.d.ts +0 -261
- package/plugins/plugin-sdk/index.js +0 -1
- package/plugins/plugin-sdk/package-lock.json +0 -12
- package/plugins/plugin-sdk/package.json +0 -22
- package/plugins/provider-claude/dobby.manifest.json +0 -17
- package/plugins/provider-claude/index.js +0 -1
- package/plugins/provider-claude/package-lock.json +0 -3398
- package/plugins/provider-claude/package.json +0 -39
- package/plugins/provider-claude/src/contribution.ts +0 -1018
- package/plugins/provider-claude/tsconfig.json +0 -19
- package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
- package/plugins/provider-claude-cli/index.js +0 -1
- package/plugins/provider-claude-cli/package-lock.json +0 -2898
- package/plugins/provider-claude-cli/package.json +0 -38
- package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
- package/plugins/provider-claude-cli/tsconfig.json +0 -19
- package/plugins/provider-pi/dobby.manifest.json +0 -17
- package/plugins/provider-pi/index.js +0 -1
- package/plugins/provider-pi/package-lock.json +0 -3877
- package/plugins/provider-pi/package.json +0 -40
- package/plugins/provider-pi/src/contribution.ts +0 -606
- package/plugins/provider-pi/tsconfig.json +0 -19
- package/plugins/sandbox-core/boxlite.js +0 -1
- package/plugins/sandbox-core/dobby.manifest.json +0 -17
- package/plugins/sandbox-core/docker.js +0 -1
- package/plugins/sandbox-core/package-lock.json +0 -136
- package/plugins/sandbox-core/package.json +0 -39
- package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
- package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
- package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
- package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
- package/plugins/sandbox-core/src/docker-executor.ts +0 -217
- package/plugins/sandbox-core/tsconfig.json +0 -19
- package/scripts/local-extensions.mjs +0 -168
- package/src/agent/event-forwarder.ts +0 -414
- package/src/cli/commands/config.ts +0 -328
- package/src/cli/commands/configure.ts +0 -92
- package/src/cli/commands/cron.ts +0 -410
- package/src/cli/commands/doctor.ts +0 -331
- package/src/cli/commands/extension.ts +0 -207
- package/src/cli/commands/init.ts +0 -211
- package/src/cli/commands/start.ts +0 -223
- package/src/cli/commands/topology.ts +0 -415
- package/src/cli/index.ts +0 -9
- package/src/cli/program.ts +0 -314
- package/src/cli/shared/config-io.ts +0 -245
- package/src/cli/shared/config-mutators.ts +0 -470
- package/src/cli/shared/config-schema.ts +0 -228
- package/src/cli/shared/config-types.ts +0 -129
- package/src/cli/shared/configure-sections.ts +0 -595
- package/src/cli/shared/discord-config.ts +0 -14
- package/src/cli/shared/init-catalog.ts +0 -249
- package/src/cli/shared/local-extension-specs.ts +0 -108
- package/src/cli/shared/runtime.ts +0 -33
- package/src/cli/shared/schema-prompts.ts +0 -443
- package/src/cli/tests/config-command.test.ts +0 -56
- package/src/cli/tests/config-io.test.ts +0 -92
- package/src/cli/tests/config-mutators.test.ts +0 -59
- package/src/cli/tests/discord-mapper.test.ts +0 -128
- package/src/cli/tests/doctor.test.ts +0 -269
- package/src/cli/tests/init-catalog.test.ts +0 -144
- package/src/cli/tests/program-options.test.ts +0 -95
- package/src/cli/tests/routing-config.test.ts +0 -281
- package/src/core/control-command.ts +0 -12
- package/src/core/dedup-store.ts +0 -103
- package/src/core/gateway.ts +0 -609
- package/src/core/routing.ts +0 -404
- package/src/core/runtime-registry.ts +0 -141
- package/src/core/tests/control-command.test.ts +0 -20
- package/src/core/tests/runtime-registry.test.ts +0 -140
- package/src/core/tests/typing-controller.test.ts +0 -129
- package/src/core/types.ts +0 -324
- package/src/core/typing-controller.ts +0 -119
- package/src/cron/config.ts +0 -154
- package/src/cron/schedule.ts +0 -61
- package/src/cron/service.ts +0 -249
- package/src/cron/store.ts +0 -155
- package/src/cron/types.ts +0 -60
- package/src/extension/loader.ts +0 -145
- package/src/extension/manager.ts +0 -355
- package/src/extension/manifest.ts +0 -26
- package/src/extension/registry.ts +0 -229
- package/src/main.ts +0 -8
- package/src/sandbox/executor.ts +0 -44
- package/src/sandbox/host-executor.ts +0 -118
- package/src/shared/dobby-repo.ts +0 -48
- 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
|
-
}
|