@nimbalyst/extension-sdk 0.2.0 → 0.2.1
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/CHANGELOG.md +13 -0
- package/dist/__tests__/createTextCollabContentAdapter.test.d.ts +2 -0
- package/dist/__tests__/createTextCollabContentAdapter.test.d.ts.map +1 -0
- package/dist/__tests__/createTextCollabContentAdapter.test.js +60 -0
- package/dist/__tests__/hasSeedableContent.test.d.ts +2 -0
- package/dist/__tests__/hasSeedableContent.test.d.ts.map +1 -0
- package/dist/__tests__/hasSeedableContent.test.js +25 -0
- package/dist/__tests__/manifestValidation.agentProviders.test.d.ts +2 -0
- package/dist/__tests__/manifestValidation.agentProviders.test.d.ts.map +1 -0
- package/dist/__tests__/manifestValidation.agentProviders.test.js +44 -0
- package/dist/agents/AgentProtocol.d.ts +227 -0
- package/dist/agents/AgentProtocol.d.ts.map +1 -0
- package/dist/agents/AgentProtocol.js +23 -0
- package/dist/agents/AgentProtocolHost.d.ts +190 -0
- package/dist/agents/AgentProtocolHost.d.ts.map +1 -0
- package/dist/agents/AgentProtocolHost.js +42 -0
- package/dist/agents/index.d.ts +26 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +24 -0
- package/dist/collab/createTextCollabContentAdapter.d.ts +26 -0
- package/dist/collab/createTextCollabContentAdapter.d.ts.map +1 -0
- package/dist/collab/createTextCollabContentAdapter.js +79 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/manifestValidation.d.ts +118 -0
- package/dist/manifestValidation.d.ts.map +1 -0
- package/dist/manifestValidation.js +475 -0
- package/dist/types/collab.d.ts +138 -0
- package/dist/types/collab.d.ts.map +1 -0
- package/dist/types/collab.js +1 -0
- package/dist/types/editor.d.ts +71 -0
- package/dist/types/editor.d.ts.map +1 -1
- package/dist/types/editors.d.ts +4 -0
- package/dist/types/editors.d.ts.map +1 -1
- package/dist/types/extension.d.ts +364 -5
- package/dist/types/extension.d.ts.map +1 -1
- package/dist/types/extension.js +11 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -0
- package/dist/types/panel.d.ts +50 -0
- package/dist/types/panel.d.ts.map +1 -1
- package/dist/types/permissions.d.ts +117 -0
- package/dist/types/permissions.d.ts.map +1 -0
- package/dist/types/permissions.js +33 -0
- package/dist/types/theme.d.ts +50 -0
- package/dist/types/theme.d.ts.map +1 -1
- package/dist/types/theme.js +7 -1
- package/dist/types/trackerImporter.d.ts +147 -0
- package/dist/types/trackerImporter.d.ts.map +1 -0
- package/dist/types/trackerImporter.js +33 -0
- package/dist/useCollaborativeEditor.d.ts +84 -30
- package/dist/useCollaborativeEditor.d.ts.map +1 -1
- package/dist/useCollaborativeEditor.js +95 -7
- package/dist/validate.browser.d.ts +5 -0
- package/dist/validate.browser.d.ts.map +1 -0
- package/dist/validate.browser.js +27 -0
- package/dist/validate.d.ts +2 -8
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +117 -0
- package/dist/validationTypes.d.ts +9 -0
- package/dist/validationTypes.d.ts.map +1 -0
- package/dist/validationTypes.js +1 -0
- package/package.json +4 -1
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure (no Node/Electron deps) validation helpers for manifest fields.
|
|
3
|
+
*
|
|
4
|
+
* Runs at both build time (via validate.ts, which reads manifest.json from
|
|
5
|
+
* disk) and runtime (when the host loads an extension and needs to refuse
|
|
6
|
+
* invalid contributions). Keep this file dependency-free so it can be bundled
|
|
7
|
+
* into either context without dragging in fs/path.
|
|
8
|
+
*/
|
|
9
|
+
import { MAX_BACKEND_MODULES_PER_EXTENSION } from './types/extension.js';
|
|
10
|
+
/**
|
|
11
|
+
* Manifest validation rejects extensions declaring more than this many AI
|
|
12
|
+
* agent providers. Chosen by symmetry with
|
|
13
|
+
* {@link MAX_BACKEND_MODULES_PER_EXTENSION}: an extension that ships more than
|
|
14
|
+
* a handful of agent providers is almost certainly mis-modeled (split it into
|
|
15
|
+
* multiple extensions, or surface them as models on one provider). The cap
|
|
16
|
+
* also keeps the settings UI -- which lists each provider as its own row --
|
|
17
|
+
* from running away.
|
|
18
|
+
*/
|
|
19
|
+
export const MAX_AGENT_PROVIDERS_PER_EXTENSION = 4;
|
|
20
|
+
const AGENT_PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
21
|
+
/**
|
|
22
|
+
* Mirrors the host's permission registry. We duplicate the list here so
|
|
23
|
+
* extensions don't have to depend on Electron internals to validate locally.
|
|
24
|
+
* Adding a new id requires updating both this list AND
|
|
25
|
+
* `packages/electron/src/main/extensions/permissionRegistry.ts`.
|
|
26
|
+
*/
|
|
27
|
+
const KNOWN_PERMISSION_IDS = [
|
|
28
|
+
'workspace-files',
|
|
29
|
+
'nimbalyst-database-read',
|
|
30
|
+
'nimbalyst-database-write',
|
|
31
|
+
'secrets-read',
|
|
32
|
+
'mcp-server-register',
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Permission ids that used to be in the catalog but never enforced anything
|
|
36
|
+
* meaningful at the backend boundary (ambient Node capabilities). Manifests
|
|
37
|
+
* that still reference them are accepted -- the validator silently drops the
|
|
38
|
+
* id from the effective list -- so we don't break older extensions during
|
|
39
|
+
* the catalog cleanup. Authors get a non-fatal warning issue back.
|
|
40
|
+
*/
|
|
41
|
+
const DEPRECATED_PERMISSION_IDS = [
|
|
42
|
+
'spawn-process',
|
|
43
|
+
'network-loopback',
|
|
44
|
+
'network-internet',
|
|
45
|
+
'filesystem',
|
|
46
|
+
];
|
|
47
|
+
const KNOWN_RUNTIMES = [
|
|
48
|
+
'utility-process',
|
|
49
|
+
'worker-thread',
|
|
50
|
+
];
|
|
51
|
+
const BACKEND_MODULE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
52
|
+
/**
|
|
53
|
+
* Validate the `contributions.backendModules` array on a manifest.
|
|
54
|
+
* Returns the list of problems found - empty means valid.
|
|
55
|
+
*
|
|
56
|
+
* Callers decide whether to treat issues as fatal (host loader: refuse the
|
|
57
|
+
* extension) or as warnings (build-time validator: print and continue with
|
|
58
|
+
* non-zero exit).
|
|
59
|
+
*/
|
|
60
|
+
export function validateBackendModules(backendModules) {
|
|
61
|
+
if (backendModules === undefined) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
if (!Array.isArray(backendModules)) {
|
|
65
|
+
return [{ message: 'contributions.backendModules must be an array' }];
|
|
66
|
+
}
|
|
67
|
+
const issues = [];
|
|
68
|
+
if (backendModules.length > MAX_BACKEND_MODULES_PER_EXTENSION) {
|
|
69
|
+
issues.push({
|
|
70
|
+
message: `contributions.backendModules declares ${backendModules.length} modules; ` +
|
|
71
|
+
`the maximum is ${MAX_BACKEND_MODULES_PER_EXTENSION}. ` +
|
|
72
|
+
'Consolidate modules to keep the consent prompt manageable.',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const seenIds = new Set();
|
|
76
|
+
for (let i = 0; i < backendModules.length; i += 1) {
|
|
77
|
+
const raw = backendModules[i];
|
|
78
|
+
if (!raw || typeof raw !== 'object') {
|
|
79
|
+
issues.push({ message: `backendModules[${i}] must be an object` });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const module = raw;
|
|
83
|
+
const moduleLabel = typeof module.id === 'string' ? module.id : `index ${i}`;
|
|
84
|
+
if (typeof module.id !== 'string' || !BACKEND_MODULE_ID_PATTERN.test(module.id)) {
|
|
85
|
+
issues.push({
|
|
86
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
87
|
+
message: `backendModules[${moduleLabel}].id must be a lowercase string matching ` +
|
|
88
|
+
`${BACKEND_MODULE_ID_PATTERN.source} (got ${JSON.stringify(module.id)})`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else if (seenIds.has(module.id)) {
|
|
92
|
+
issues.push({
|
|
93
|
+
moduleId: module.id,
|
|
94
|
+
message: `backendModules contains duplicate id "${module.id}"`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
seenIds.add(module.id);
|
|
99
|
+
}
|
|
100
|
+
if (typeof module.entry !== 'string' || module.entry.length === 0) {
|
|
101
|
+
issues.push({
|
|
102
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
103
|
+
message: `backendModules[${moduleLabel}].entry must be a non-empty relative path string`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else if (module.entry.startsWith('/') || module.entry.includes('..')) {
|
|
107
|
+
// Refuse absolute paths and parent-directory traversal so a module
|
|
108
|
+
// can't escape its extension root. The host resolves entry relative
|
|
109
|
+
// to the extension directory; only safe relative paths belong here.
|
|
110
|
+
issues.push({
|
|
111
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
112
|
+
message: `backendModules[${moduleLabel}].entry must be a relative path within the extension root ` +
|
|
113
|
+
`(no leading "/", no ".." segments)`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (!KNOWN_RUNTIMES.includes(module.runtime)) {
|
|
117
|
+
issues.push({
|
|
118
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
119
|
+
message: `backendModules[${moduleLabel}].runtime must be one of: ${KNOWN_RUNTIMES.join(', ')} ` +
|
|
120
|
+
`(got ${JSON.stringify(module.runtime)})`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (module.permissions !== undefined && !Array.isArray(module.permissions)) {
|
|
124
|
+
issues.push({
|
|
125
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
126
|
+
message: `backendModules[${moduleLabel}].permissions must be an array of ` +
|
|
127
|
+
`host-brokered capability ids (or omitted). The implicit "run native ` +
|
|
128
|
+
`code" grant is conferred by enabling the module itself, not by an ` +
|
|
129
|
+
`entry in this array.`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else if (Array.isArray(module.permissions)) {
|
|
133
|
+
const uniquePermissions = new Set();
|
|
134
|
+
for (const permission of module.permissions) {
|
|
135
|
+
if (typeof permission !== 'string') {
|
|
136
|
+
issues.push({
|
|
137
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
138
|
+
message: `backendModules[${moduleLabel}].permissions contains non-string entry`,
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (DEPRECATED_PERMISSION_IDS.includes(permission)) {
|
|
143
|
+
// Older manifests still ship these. They never meaningfully gated
|
|
144
|
+
// anything inside the backend runtime (ambient Node access), and
|
|
145
|
+
// the catalog cleanup removed them. Warn the author and silently
|
|
146
|
+
// drop the id from the effective list.
|
|
147
|
+
issues.push({
|
|
148
|
+
severity: 'warning',
|
|
149
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
150
|
+
message: `backendModules[${moduleLabel}].permissions includes deprecated id "${permission}". ` +
|
|
151
|
+
`This id is unenforceable inside a Node backend (the module can require child_process/fs/net ` +
|
|
152
|
+
`directly) and has been removed from the catalog. Granting the module is itself the consent ` +
|
|
153
|
+
`to run native code; drop this id from your manifest.`,
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!KNOWN_PERMISSION_IDS.includes(permission)) {
|
|
158
|
+
issues.push({
|
|
159
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
160
|
+
message: `backendModules[${moduleLabel}].permissions contains unknown id "${permission}". ` +
|
|
161
|
+
`Valid ids: ${KNOWN_PERMISSION_IDS.join(', ')}`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (uniquePermissions.has(permission)) {
|
|
165
|
+
issues.push({
|
|
166
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
167
|
+
message: `backendModules[${moduleLabel}].permissions contains duplicate "${permission}"`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
uniquePermissions.add(permission);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const enablement = module.enablement;
|
|
174
|
+
if (!enablement || typeof enablement !== 'object') {
|
|
175
|
+
issues.push({
|
|
176
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
177
|
+
message: `backendModules[${moduleLabel}].enablement is required`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
if (enablement.default !== 'disabled') {
|
|
182
|
+
issues.push({
|
|
183
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
184
|
+
message: `backendModules[${moduleLabel}].enablement.default must be "disabled". ` +
|
|
185
|
+
'Privileged capabilities are always opt-in.',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (enablement.promptOn !== 'firstUse') {
|
|
189
|
+
issues.push({
|
|
190
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
191
|
+
message: `backendModules[${moduleLabel}].enablement.promptOn must be "firstUse"`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (typeof enablement.purpose !== 'string' || enablement.purpose.trim().length === 0) {
|
|
195
|
+
issues.push({
|
|
196
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
197
|
+
message: `backendModules[${moduleLabel}].enablement.purpose must be a non-empty string. ` +
|
|
198
|
+
'This is shown verbatim in the consent prompt - write it from the user\'s perspective.',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else if (enablement.purpose.length > 280) {
|
|
202
|
+
issues.push({
|
|
203
|
+
moduleId: typeof module.id === 'string' ? module.id : undefined,
|
|
204
|
+
message: `backendModules[${moduleLabel}].enablement.purpose is too long (${enablement.purpose.length} chars). ` +
|
|
205
|
+
'Keep it under 280 characters; long copy doesn\'t fit the consent prompt.',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return issues;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Convenience wrapper that throws if any fatal issues are found. Warnings
|
|
214
|
+
* (currently: deprecated permission ids) are surfaced via the issues array
|
|
215
|
+
* but never throw, so an extension that still references `spawn-process`
|
|
216
|
+
* keeps loading -- the host just drops the id when computing effective
|
|
217
|
+
* permissions.
|
|
218
|
+
*
|
|
219
|
+
* Use this in main-process load paths where invalid manifests should refuse
|
|
220
|
+
* the extension outright.
|
|
221
|
+
*/
|
|
222
|
+
export function assertBackendModulesValid(extensionId, backendModules) {
|
|
223
|
+
const issues = validateBackendModules(backendModules);
|
|
224
|
+
const fatal = issues.filter((i) => i.severity !== 'warning');
|
|
225
|
+
if (fatal.length === 0) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const lines = fatal.map((i) => i.moduleId ? ` - [${i.moduleId}] ${i.message}` : ` - ${i.message}`);
|
|
229
|
+
throw new Error(`Extension ${extensionId} has invalid backendModules declarations:\n${lines.join('\n')}`);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Filter a raw `permissions` array on a backend-module contribution down to
|
|
233
|
+
* the ids the host actually understands. Deprecated catalog ids
|
|
234
|
+
* (`spawn-process`, `network-loopback`, `network-internet`, `filesystem`)
|
|
235
|
+
* are dropped silently -- they never gated anything inside the backend
|
|
236
|
+
* runtime, so the host treats them as no-ops. Unknown ids are also dropped;
|
|
237
|
+
* `validateBackendModules` raises a separate error for those, so the loader
|
|
238
|
+
* will refuse the module before this is consulted in earnest.
|
|
239
|
+
*/
|
|
240
|
+
export function effectiveModulePermissions(raw) {
|
|
241
|
+
if (!Array.isArray(raw))
|
|
242
|
+
return [];
|
|
243
|
+
const out = [];
|
|
244
|
+
const seen = new Set();
|
|
245
|
+
for (const entry of raw) {
|
|
246
|
+
if (typeof entry !== 'string')
|
|
247
|
+
continue;
|
|
248
|
+
if (seen.has(entry))
|
|
249
|
+
continue;
|
|
250
|
+
seen.add(entry);
|
|
251
|
+
if (KNOWN_PERMISSION_IDS.includes(entry)) {
|
|
252
|
+
out.push(entry);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Extract the set of declared backend-module ids from a manifest's
|
|
259
|
+
* `contributions.backendModules` array. Used by
|
|
260
|
+
* {@link validateAgentProviders} so the `backendModuleId` cross-check does
|
|
261
|
+
* not need the caller to pre-compute the set.
|
|
262
|
+
*
|
|
263
|
+
* Returns an empty set if `backendModules` is missing or malformed. Exported
|
|
264
|
+
* so build tooling can reuse the same extraction logic when checking entry
|
|
265
|
+
* files against the declared modules.
|
|
266
|
+
*/
|
|
267
|
+
export function extractBackendModuleIds(backendModules) {
|
|
268
|
+
const ids = new Set();
|
|
269
|
+
if (!Array.isArray(backendModules))
|
|
270
|
+
return ids;
|
|
271
|
+
for (const raw of backendModules) {
|
|
272
|
+
if (!raw || typeof raw !== 'object')
|
|
273
|
+
continue;
|
|
274
|
+
const id = raw.id;
|
|
275
|
+
if (typeof id === 'string' && id.length > 0) {
|
|
276
|
+
ids.add(id);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return ids;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Validate the `contributions.aiAgentProviders` array on a manifest.
|
|
283
|
+
*
|
|
284
|
+
* AI agent providers are surfaced in Nimbalyst's chat / agent UI as selectable
|
|
285
|
+
* providers (alongside built-in Claude Code, Claude, OpenAI, etc.). Each
|
|
286
|
+
* provider is implemented by a backend module that runs outside the renderer
|
|
287
|
+
* (so it can drive a CLI, hold long-lived processes, or speak a custom
|
|
288
|
+
* protocol). The contribution shape, in practice:
|
|
289
|
+
*
|
|
290
|
+
* {
|
|
291
|
+
* id: string, // unique within the extension
|
|
292
|
+
* displayName: string, // shown in the provider dropdown
|
|
293
|
+
* backendModuleId: string, // must match a declared backendModule
|
|
294
|
+
* models?: Array<{ id, name, ... }> // models the provider advertises
|
|
295
|
+
* toolFileLinks?: { [toolName]: ... } // optional UI hint table
|
|
296
|
+
* }
|
|
297
|
+
*
|
|
298
|
+
* This validator checks the manifest-side wiring; it does not introspect the
|
|
299
|
+
* bundle. See {@link validateExtensionBundle} (Node-only) for the bundle-side
|
|
300
|
+
* check that the referenced backend module actually exports an
|
|
301
|
+
* `activate`/`createAgentProvider` function.
|
|
302
|
+
*
|
|
303
|
+
* Returns the list of problems found - empty means valid.
|
|
304
|
+
*/
|
|
305
|
+
export function validateAgentProviders(aiAgentProviders, backendModules) {
|
|
306
|
+
if (aiAgentProviders === undefined) {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
if (!Array.isArray(aiAgentProviders)) {
|
|
310
|
+
return [
|
|
311
|
+
{ message: 'contributions.aiAgentProviders must be an array' },
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
const issues = [];
|
|
315
|
+
if (aiAgentProviders.length > MAX_AGENT_PROVIDERS_PER_EXTENSION) {
|
|
316
|
+
issues.push({
|
|
317
|
+
message: `contributions.aiAgentProviders declares ${aiAgentProviders.length} providers; ` +
|
|
318
|
+
`the maximum is ${MAX_AGENT_PROVIDERS_PER_EXTENSION}. ` +
|
|
319
|
+
'Split into multiple extensions, or surface variants as models on a single provider.',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
const knownBackendModuleIds = extractBackendModuleIds(backendModules);
|
|
323
|
+
const seenProviderIds = new Set();
|
|
324
|
+
for (let i = 0; i < aiAgentProviders.length; i += 1) {
|
|
325
|
+
const raw = aiAgentProviders[i];
|
|
326
|
+
if (!raw || typeof raw !== 'object') {
|
|
327
|
+
issues.push({
|
|
328
|
+
message: `aiAgentProviders[${i}] must be an object`,
|
|
329
|
+
});
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const provider = raw;
|
|
333
|
+
const providerLabel = typeof provider.id === 'string' && provider.id.length > 0
|
|
334
|
+
? provider.id
|
|
335
|
+
: `index ${i}`;
|
|
336
|
+
// (b) id presence + shape + uniqueness within the extension.
|
|
337
|
+
if (typeof provider.id !== 'string' ||
|
|
338
|
+
!AGENT_PROVIDER_ID_PATTERN.test(provider.id)) {
|
|
339
|
+
issues.push({
|
|
340
|
+
providerId: typeof provider.id === 'string' ? provider.id : undefined,
|
|
341
|
+
message: `aiAgentProviders[${providerLabel}].id must be a lowercase string matching ` +
|
|
342
|
+
`${AGENT_PROVIDER_ID_PATTERN.source} (got ${JSON.stringify(provider.id)})`,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
else if (seenProviderIds.has(provider.id)) {
|
|
346
|
+
issues.push({
|
|
347
|
+
providerId: provider.id,
|
|
348
|
+
message: `aiAgentProviders contains duplicate id "${provider.id}"`,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
seenProviderIds.add(provider.id);
|
|
353
|
+
}
|
|
354
|
+
// displayName: non-empty string. Surfaced verbatim in the provider
|
|
355
|
+
// dropdown, so it has to be readable; not currently length-capped because
|
|
356
|
+
// we have no signal yet on what the UI tolerates. Matches the
|
|
357
|
+
// AiAgentProviderContribution type, which names this field `displayName`.
|
|
358
|
+
if (typeof provider.displayName !== 'string' || provider.displayName.trim().length === 0) {
|
|
359
|
+
issues.push({
|
|
360
|
+
providerId: typeof provider.id === 'string' ? provider.id : undefined,
|
|
361
|
+
message: `aiAgentProviders[${providerLabel}].displayName must be a non-empty string ` +
|
|
362
|
+
'(shown verbatim in the provider dropdown).',
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
// (a) backendModuleId must reference a real backendModules[*].id.
|
|
366
|
+
if (typeof provider.backendModuleId !== 'string' ||
|
|
367
|
+
provider.backendModuleId.length === 0) {
|
|
368
|
+
issues.push({
|
|
369
|
+
providerId: typeof provider.id === 'string' ? provider.id : undefined,
|
|
370
|
+
message: `aiAgentProviders[${providerLabel}].backendModuleId must be a non-empty string ` +
|
|
371
|
+
'referencing a backendModules[*].id declared on the same manifest.',
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
else if (!knownBackendModuleIds.has(provider.backendModuleId)) {
|
|
375
|
+
issues.push({
|
|
376
|
+
providerId: typeof provider.id === 'string' ? provider.id : undefined,
|
|
377
|
+
message: `aiAgentProviders[${providerLabel}].backendModuleId "${provider.backendModuleId}" ` +
|
|
378
|
+
'does not match any contributions.backendModules[*].id. ' +
|
|
379
|
+
'Agent providers must be implemented by a declared backend module on the same extension.',
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
// (c) models, if present, must be an array of { id, name }. These are
|
|
383
|
+
// the only required fields on the AiAgentProviderModel type; the host
|
|
384
|
+
// derives the provider from the contribution id, so models carry no
|
|
385
|
+
// per-entry provider field.
|
|
386
|
+
if (provider.models !== undefined) {
|
|
387
|
+
if (!Array.isArray(provider.models)) {
|
|
388
|
+
issues.push({
|
|
389
|
+
providerId: typeof provider.id === 'string' ? provider.id : undefined,
|
|
390
|
+
message: `aiAgentProviders[${providerLabel}].models must be an array (or omitted).`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
const seenModelIds = new Set();
|
|
395
|
+
for (let m = 0; m < provider.models.length; m += 1) {
|
|
396
|
+
const rawModel = provider.models[m];
|
|
397
|
+
const modelLabel = `models[${m}]`;
|
|
398
|
+
if (!rawModel || typeof rawModel !== 'object') {
|
|
399
|
+
issues.push({
|
|
400
|
+
providerId: typeof provider.id === 'string' ? provider.id : undefined,
|
|
401
|
+
message: `aiAgentProviders[${providerLabel}].${modelLabel} must be an object`,
|
|
402
|
+
});
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const model = rawModel;
|
|
406
|
+
if (typeof model.id !== 'string' ||
|
|
407
|
+
model.id.trim().length === 0) {
|
|
408
|
+
issues.push({
|
|
409
|
+
providerId: typeof provider.id === 'string' ? provider.id : undefined,
|
|
410
|
+
message: `aiAgentProviders[${providerLabel}].${modelLabel}.id must be a non-empty string`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
else if (seenModelIds.has(model.id)) {
|
|
414
|
+
issues.push({
|
|
415
|
+
providerId: typeof provider.id === 'string' ? provider.id : undefined,
|
|
416
|
+
message: `aiAgentProviders[${providerLabel}].models contains duplicate id "${model.id}"`,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
seenModelIds.add(model.id);
|
|
421
|
+
}
|
|
422
|
+
if (typeof model.name !== 'string' ||
|
|
423
|
+
model.name.trim().length === 0) {
|
|
424
|
+
issues.push({
|
|
425
|
+
providerId: typeof provider.id === 'string' ? provider.id : undefined,
|
|
426
|
+
message: `aiAgentProviders[${providerLabel}].${modelLabel}.name must be a non-empty string`,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// (d) toolFileLinks, if present, must be a plain object whose keys are
|
|
433
|
+
// non-empty strings (tool names). We deliberately do NOT validate the
|
|
434
|
+
// values: the host-side schema for the link payload is still in flux,
|
|
435
|
+
// and rejecting unknown value shapes here would force every shape change
|
|
436
|
+
// through this SDK file. Keys are the only contract this validator
|
|
437
|
+
// pins.
|
|
438
|
+
if (provider.toolFileLinks !== undefined) {
|
|
439
|
+
if (typeof provider.toolFileLinks !== 'object' ||
|
|
440
|
+
provider.toolFileLinks === null ||
|
|
441
|
+
Array.isArray(provider.toolFileLinks)) {
|
|
442
|
+
issues.push({
|
|
443
|
+
providerId: typeof provider.id === 'string' ? provider.id : undefined,
|
|
444
|
+
message: `aiAgentProviders[${providerLabel}].toolFileLinks must be a plain object ` +
|
|
445
|
+
'mapping tool names to link descriptors (or omitted).',
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
for (const key of Object.keys(provider.toolFileLinks)) {
|
|
450
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
451
|
+
issues.push({
|
|
452
|
+
providerId: typeof provider.id === 'string' ? provider.id : undefined,
|
|
453
|
+
message: `aiAgentProviders[${providerLabel}].toolFileLinks contains an empty/invalid tool name key`,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return issues;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Convenience wrapper that throws if any fatal aiAgentProviders issues are
|
|
464
|
+
* found. Mirrors {@link assertBackendModulesValid}. Use this in main-process
|
|
465
|
+
* load paths where invalid manifests should refuse the extension outright.
|
|
466
|
+
*/
|
|
467
|
+
export function assertAgentProvidersValid(extensionId, aiAgentProviders, backendModules) {
|
|
468
|
+
const issues = validateAgentProviders(aiAgentProviders, backendModules);
|
|
469
|
+
const fatal = issues.filter((i) => i.severity !== 'warning');
|
|
470
|
+
if (fatal.length === 0) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const lines = fatal.map((i) => i.providerId ? ` - [${i.providerId}] ${i.message}` : ` - ${i.message}`);
|
|
474
|
+
throw new Error(`Extension ${extensionId} has invalid aiAgentProviders declarations:\n${lines.join('\n')}`);
|
|
475
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CollabCodec
|
|
3
|
+
*
|
|
4
|
+
* Per-extension Y.Doc content contract: the single, PURE thing an editor
|
|
5
|
+
* defines for collaboration. Pure functions `file bytes <-> Y.Doc shape` (no
|
|
6
|
+
* React, no host imports) so the same code seeds a live editor AND runs
|
|
7
|
+
* headlessly (Share-to-Team without the editor open, re-upload from local
|
|
8
|
+
* origin, plain-text projection for search/AI). It lets host features
|
|
9
|
+
* (re-upload, history, export, AI editing, search indexing, comments, backup,
|
|
10
|
+
* restore) operate on any extension's collaborative document without knowing
|
|
11
|
+
* its internal layout.
|
|
12
|
+
*
|
|
13
|
+
* Extensions register a codec via
|
|
14
|
+
* `context.services.collab.registerContentAdapter(...)` from their
|
|
15
|
+
* `activate()` function, and pass the SAME codec to
|
|
16
|
+
* `useCollaborativeEditor(host, { codec, bind })`. `isEmpty` / `seedFromFile`
|
|
17
|
+
* / `exportToFile` are therefore defined exactly ONCE, on the codec -- the
|
|
18
|
+
* live seed and the headless seed are provably the same code.
|
|
19
|
+
*
|
|
20
|
+
* Codecs run only client-side (main process, renderer, extensions). The collab
|
|
21
|
+
* Worker stays codec-agnostic and treats Y.Doc state as opaque ciphertext. The
|
|
22
|
+
* renderer registry is authoritative; the main-process registry is an optional
|
|
23
|
+
* cache (in-repo statics + text descriptors) and its absence degrades to
|
|
24
|
+
* client seeding, never a hard error.
|
|
25
|
+
*
|
|
26
|
+
* NOTE: This is the canonical definition. The host-internal
|
|
27
|
+
* `@nimbalyst/collab-adapters` registry re-imports this type --
|
|
28
|
+
* do not fork the interface there.
|
|
29
|
+
*
|
|
30
|
+
* @see CollabContentAdapter -- the former name, kept as a deprecated alias.
|
|
31
|
+
*/
|
|
32
|
+
import type { Doc } from 'yjs';
|
|
33
|
+
export type CollabContentFileSource = string | Uint8Array;
|
|
34
|
+
/**
|
|
35
|
+
* Serializable description of a CollabContentAdapter that lets the HOST
|
|
36
|
+
* replicate the adapter in another process (e.g. the Electron main process)
|
|
37
|
+
* WITHOUT loading the extension's code or doing a dynamic import. Only adapters
|
|
38
|
+
* built from a known host factory carry one. Today the single kind is `text`
|
|
39
|
+
* (a one-`Y.Text` document, produced by `createTextCollabContentAdapter`).
|
|
40
|
+
*/
|
|
41
|
+
export interface TextCollabAdapterDescriptor {
|
|
42
|
+
kind: 'text';
|
|
43
|
+
documentType: string;
|
|
44
|
+
fileExtensions: string[];
|
|
45
|
+
mimeType?: string;
|
|
46
|
+
textField: string;
|
|
47
|
+
layoutVersion: number;
|
|
48
|
+
}
|
|
49
|
+
export type CollabAdapterDescriptor = TextCollabAdapterDescriptor;
|
|
50
|
+
export interface CollabCodecMigration {
|
|
51
|
+
from: number;
|
|
52
|
+
to: number;
|
|
53
|
+
run(yDoc: Doc): void;
|
|
54
|
+
}
|
|
55
|
+
/** @deprecated Renamed to {@link CollabCodecMigration}. */
|
|
56
|
+
export type CollabContentAdapterMigration = CollabCodecMigration;
|
|
57
|
+
export interface CollabCodec<TStructured = unknown> {
|
|
58
|
+
/** Identifies this codec; matches the shared doc's documentType. */
|
|
59
|
+
documentType: string;
|
|
60
|
+
/** File extensions this adapter is the on-disk codec for. Include
|
|
61
|
+
* the leading dot (e.g. '.md', '.mockup.html'). The first entry is
|
|
62
|
+
* used as the default by save-a-copy / export-to-file flows. */
|
|
63
|
+
fileExtensions: string[];
|
|
64
|
+
/** Optional MIME type used by save dialogs and asset uploads. */
|
|
65
|
+
mimeType?: string;
|
|
66
|
+
/** Layout schema version. Bump when the Y.Doc shape changes; pair
|
|
67
|
+
* with `migrations` to migrate older docs forward before any
|
|
68
|
+
* write op. */
|
|
69
|
+
layoutVersion: number;
|
|
70
|
+
/** Optional migrations from older layout versions. Run by the
|
|
71
|
+
* registry before `applyFromFile` / `applyStructuredPatch` when
|
|
72
|
+
* the Y.Doc's recorded layoutVersion is older than this adapter's
|
|
73
|
+
* layoutVersion. */
|
|
74
|
+
migrations?: CollabCodecMigration[];
|
|
75
|
+
/** True iff the Y.Doc has no extension content yet. Used to gate
|
|
76
|
+
* the initial-share seed flow. Shared by the live seed (the hook)
|
|
77
|
+
* and every headless seed path, so they can never disagree. */
|
|
78
|
+
isEmpty(yDoc: Doc): boolean;
|
|
79
|
+
/** Seed an empty Y.Doc from on-disk file bytes/text. Initial share
|
|
80
|
+
* only -- adapters can assume `isEmpty(yDoc) === true`. */
|
|
81
|
+
seedFromFile(yDoc: Doc, source: CollabContentFileSource): void;
|
|
82
|
+
/** Replace Y.Doc content with the supplied on-disk file content.
|
|
83
|
+
* Must be safe to call on a populated Y.Doc. Default behaviour is
|
|
84
|
+
* wipe-and-reseed inside a single Y.Doc transaction. Adapters
|
|
85
|
+
* that want finer-grained history can override with a
|
|
86
|
+
* diff-and-patch implementation. */
|
|
87
|
+
applyFromFile(yDoc: Doc, source: CollabContentFileSource): void;
|
|
88
|
+
/** Serialize the live Y.Doc back to the on-disk file format. */
|
|
89
|
+
exportToFile(yDoc: Doc): string | Uint8Array;
|
|
90
|
+
/** Plain-text projection for search, AI prompts, diffs, history
|
|
91
|
+
* previews. Lossy is fine; this is not a round-trip channel. */
|
|
92
|
+
toPlainText(yDoc: Doc): string;
|
|
93
|
+
/** Optional: structured projection for AI tool-call edits and
|
|
94
|
+
* comment anchoring. Shape is extension-defined. Paired with
|
|
95
|
+
* `applyStructuredPatch`. */
|
|
96
|
+
toStructured?(yDoc: Doc): TStructured;
|
|
97
|
+
/** Optional: write structured edits back. Paired with
|
|
98
|
+
* `toStructured`. AI-write surface is gated on the presence of
|
|
99
|
+
* both this and `toStructured`. */
|
|
100
|
+
applyStructuredPatch?(yDoc: Doc, patch: unknown): void;
|
|
101
|
+
/** Optional: produce a snapshot for revision history. Defaults to
|
|
102
|
+
* `Y.encodeStateAsUpdateV2(yDoc)`. Override if you need a denser
|
|
103
|
+
* snapshot format. */
|
|
104
|
+
exportRevisionSnapshot?(yDoc: Doc): Uint8Array;
|
|
105
|
+
/** Optional: restore a revision snapshot. Defaults to
|
|
106
|
+
* `Y.applyUpdateV2(yDoc, bytes)`. */
|
|
107
|
+
restoreRevisionSnapshot?(yDoc: Doc, bytes: Uint8Array): void;
|
|
108
|
+
/** Optional: a serializable descriptor the host can ship to another process
|
|
109
|
+
* to rebuild this adapter without the extension's code. Set automatically by
|
|
110
|
+
* host factories like `createTextCollabContentAdapter`; hand-written
|
|
111
|
+
* adapters may leave it undefined (they stay process-local). */
|
|
112
|
+
serializableDescriptor?: CollabAdapterDescriptor;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* @deprecated Renamed to {@link CollabCodec}. Same shape -- a codec is a pure
|
|
116
|
+
* `file bytes <-> Y.Doc shape` contract. Kept so existing extensions and the
|
|
117
|
+
* host-internal `@nimbalyst/collab-adapters` registry keep compiling.
|
|
118
|
+
*/
|
|
119
|
+
export type CollabContentAdapter<TStructured = unknown> = CollabCodec<TStructured>;
|
|
120
|
+
/**
|
|
121
|
+
* The collab surface on the extension context. Extensions call into
|
|
122
|
+
* this from their `activate()` to register a collab codec for any
|
|
123
|
+
* document type they ship.
|
|
124
|
+
*/
|
|
125
|
+
export interface ExtensionCollabService {
|
|
126
|
+
/**
|
|
127
|
+
* Register a CollabCodec for one of the extension's document types.
|
|
128
|
+
* Returns a Disposable that unregisters on deactivation; the host also
|
|
129
|
+
* tracks the registration in `context.subscriptions` automatically.
|
|
130
|
+
*
|
|
131
|
+
* (Named `registerContentAdapter` for backwards compatibility; it accepts a
|
|
132
|
+
* {@link CollabCodec}.)
|
|
133
|
+
*/
|
|
134
|
+
registerContentAdapter(codec: CollabCodec): {
|
|
135
|
+
dispose(): void;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=collab.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collab.d.ts","sourceRoot":"","sources":["../../src/types/collab.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE/B,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,UAAU,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,uBAAuB,GAAG,2BAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;CACtB;AAED,2DAA2D;AAC3D,MAAM,MAAM,6BAA6B,GAAG,oBAAoB,CAAC;AAEjE,MAAM,WAAW,WAAW,CAAC,WAAW,GAAG,OAAO;IAChD,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;IAErB;;qEAEiE;IACjE,cAAc,EAAE,MAAM,EAAE,CAAC;IAEzB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;oBAEgB;IAChB,aAAa,EAAE,MAAM,CAAC;IAEtB;;;yBAGqB;IACrB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAEpC;;oEAEgE;IAChE,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC;IAE5B;gEAC4D;IAC5D,YAAY,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAE/D;;;;yCAIqC;IACrC,aAAa,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAEhE,gEAAgE;IAChE,YAAY,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,GAAG,UAAU,CAAC;IAE7C;qEACiE;IACjE,WAAW,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,CAAC;IAE/B;;kCAE8B;IAC9B,YAAY,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG,WAAW,CAAC;IAEtC;;wCAEoC;IACpC,oBAAoB,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAEvD;;2BAEuB;IACvB,sBAAsB,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG,UAAU,CAAC;IAE/C;0CACsC;IACtC,uBAAuB,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAE7D;;;qEAGiE;IACjE,sBAAsB,CAAC,EAAE,uBAAuB,CAAC;CAClD;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,CAAC,WAAW,GAAG,OAAO,IAAI,WAAW,CAAC,WAAW,CAAC,CAAC;AAEnF;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,sBAAsB,CAAC,KAAK,EAAE,WAAW,GAAG;QAAE,OAAO,IAAI,IAAI,CAAA;KAAE,CAAC;CACjE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|