@capybara-hq/kernel 0.1.0-rc.20 → 0.1.0-rc.21
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/bootstrap.json +10 -1
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/extensions/bootstrap.d.ts +49 -10
- package/dist/extensions/bootstrap.js +104 -27
- package/dist/lifecycle/kernel-lifecycle.d.ts +5 -1
- package/dist/lifecycle/kernel-lifecycle.js +16 -4
- package/package.json +2 -2
package/bootstrap.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"schemaVersion": 1,
|
|
4
4
|
"marketplace": {
|
|
5
5
|
"url": "https://github.com/capybara-hq/plugins.git",
|
|
6
|
-
"commit": "
|
|
6
|
+
"commit": "050de1403662d2ba04cb96839b742aaa0166c00e"
|
|
7
7
|
},
|
|
8
8
|
"plugins": [
|
|
9
9
|
{
|
|
@@ -101,6 +101,15 @@
|
|
|
101
101
|
"version": "0.1.0-rc.6",
|
|
102
102
|
"integrity": "sha512-AOKLpoaBfOQUWnP9nszg9DQWR+6z5gl+ZHoTlHysA9LAeC2h/zipjGE9hY69HF9rueSHzM2kGESJLvXIuQODAA==",
|
|
103
103
|
"required": false
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "skills-core",
|
|
107
|
+
"source": "npm",
|
|
108
|
+
"npm": "@capybara-hq/plugin-skills-core",
|
|
109
|
+
"version": "0.1.0-rc.0",
|
|
110
|
+
"integrity": "sha512-U3R+B3QcklCQyye22bQU1aJq05oLW4Fr3hfmLHuPFOq5jNt30mDc0zNPSDLr6MFDdUI/OJkkX7wvkyKa+zRPAQ==",
|
|
111
|
+
"required": false,
|
|
112
|
+
"builtin": true
|
|
104
113
|
}
|
|
105
114
|
]
|
|
106
115
|
}
|
|
@@ -188,7 +188,7 @@ export declare function headsToResolve(entries: readonly MarketplaceEntry[], imp
|
|
|
188
188
|
/** The host versions (an entry's `minKernel` is judged against them). */
|
|
189
189
|
export declare const HOST_VERSIONS: {
|
|
190
190
|
readonly kernel: string;
|
|
191
|
-
readonly sdk: "0.1.0-rc.
|
|
191
|
+
readonly sdk: "0.1.0-rc.15";
|
|
192
192
|
};
|
|
193
193
|
/** Whether this deployment satisfies the entry's `minKernel`. */
|
|
194
194
|
export declare const entryCompatible: (e: Pick<MarketplaceEntry, "minKernel">) => boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type BootstrapManifest, type Logger } from "@capybara-hq/plugin-sdk";
|
|
1
|
+
import { type BootstrapEntry, type BootstrapManifest, type Logger } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import { type PluginImport } from "../catalog/plugin-import.js";
|
|
3
3
|
import type { KernelConfig } from "../config/schema.js";
|
|
4
|
+
import { ApiError } from "../errors.js";
|
|
4
5
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
5
6
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
6
7
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
@@ -35,9 +36,15 @@ import type { SingleProviderNote } from "./single-provider.js";
|
|
|
35
36
|
* until expiry) aborts the run at its next step. "Done" is a `bootstrap_runs` row for the
|
|
36
37
|
* manifest's sha256 (written with the audit `bootstrap.completed` only when every entry
|
|
37
38
|
* succeeded; otherwise the next start runs it again, which is idempotent).
|
|
39
|
+
* - Two passes per start: the first runs before the extensions load (no object store yet), the
|
|
40
|
+
* second after them. A `builtin` entry (not required, but builtin in the catalog) is deferred by
|
|
41
|
+
* the first pass (`deferBuiltin`: skipped, not a failure; the run is then not recorded and the
|
|
42
|
+
* start runs the second pass), so it may ship skills and templates. A required entry is imported
|
|
43
|
+
* by the first pass and must succeed; a builtin one that fails is logged like an optional one.
|
|
38
44
|
* - Only entries new to the manifest are installed (Ruling 5): an optional entry that the last
|
|
39
45
|
* completed run already handled and that is not installed now (a super admin uninstalled
|
|
40
|
-
* it) stays uninstalled -> `kept`. A required entry is always installed
|
|
46
|
+
* it) stays uninstalled -> `kept`. A required or builtin entry is always installed (a
|
|
47
|
+
* declarative one imported again when its catalog item is gone).
|
|
41
48
|
* - Per entry, by the installed state of its extension:
|
|
42
49
|
* - not installed (and new, or required): imported into the catalog (source `bootstrap`, ref
|
|
43
50
|
* `bootstrap:<name>@<version>`, like a marketplace import), installed, approved (snapshot
|
|
@@ -65,7 +72,8 @@ import type { SingleProviderNote } from "./single-provider.js";
|
|
|
65
72
|
* never needed again. A plugin's icons and setup guide files are kept in PG too (`catalog_files`),
|
|
66
73
|
* so they never need it either; its skills, connector templates and agent templates do (an entry
|
|
67
74
|
* shipping them fails while no object store runs; an optional one is retried after the extensions
|
|
68
|
-
* loaded
|
|
75
|
+
* loaded, and a plugin that must be builtin and ships them is a `builtin` entry, which only the
|
|
76
|
+
* pass after the extensions load imports).
|
|
69
77
|
*/
|
|
70
78
|
/** The bootstrap manifest that ships with the kernel package. */
|
|
71
79
|
export declare const BOOTSTRAP_MANIFEST_FILE: string;
|
|
@@ -93,15 +101,23 @@ export interface BootstrapFailure {
|
|
|
93
101
|
code: string;
|
|
94
102
|
message: string;
|
|
95
103
|
}
|
|
96
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Some entries failed or were deferred (the others were done); the run is not recorded as done.
|
|
106
|
+
*/
|
|
97
107
|
export declare class BootstrapFailedError extends Error {
|
|
98
108
|
readonly failures: BootstrapFailure[];
|
|
99
109
|
readonly results: BootstrapEntryResult[];
|
|
110
|
+
readonly deferred: string[];
|
|
111
|
+
readonly packages: ReadonlyMap<string, Uint8Array>;
|
|
100
112
|
/**
|
|
101
113
|
* @param results what the entries that did not fail did (pass them as `carry` to a later run in
|
|
102
114
|
* the same start, so its record keeps `installed` instead of `kept`).
|
|
115
|
+
* @param deferred the builtin entries a first pass skipped (`deferBuiltin`): not failures, the
|
|
116
|
+
* later pass of the same start imports them.
|
|
117
|
+
* @param packages the packages this run obtained, by entry name (pass them as `packages` to the
|
|
118
|
+
* later pass of the same start, so nothing is downloaded twice).
|
|
103
119
|
*/
|
|
104
|
-
constructor(failures: BootstrapFailure[], results?: BootstrapEntryResult[]);
|
|
120
|
+
constructor(failures: BootstrapFailure[], results?: BootstrapEntryResult[], deferred?: string[], packages?: ReadonlyMap<string, Uint8Array>);
|
|
105
121
|
/** A required entry failed: the kernel must not serve without it. */
|
|
106
122
|
get requiredFailed(): boolean;
|
|
107
123
|
}
|
|
@@ -121,13 +137,21 @@ export interface BootstrapResolveOptions {
|
|
|
121
137
|
log?: Pick<Logger, "warn">;
|
|
122
138
|
/** The npm download (default {@link fetchNpmPackage}; tests replace it). */
|
|
123
139
|
fetch?: typeof fetchNpmPackage;
|
|
140
|
+
/**
|
|
141
|
+
* Entries whose package failing to be obtained is that entry's failure only: the error goes into
|
|
142
|
+
* `failed` (by entry name) and the other entries are still obtained. Every other entry's failure
|
|
143
|
+
* throws, so nothing is written.
|
|
144
|
+
*/
|
|
145
|
+
soft?: (e: BootstrapEntry) => boolean;
|
|
146
|
+
failed?: Map<string, ApiError>;
|
|
124
147
|
}
|
|
125
148
|
/**
|
|
126
149
|
* Obtains every entry's tgz, checked against its pinned sha512: from the offline directory when a
|
|
127
150
|
* file there has that sha512, else (a `source: "npm"` entry) downloaded from the npm registry and
|
|
128
151
|
* verified against the pin (the packument's `dist.integrity` is not consulted). Throws an
|
|
129
152
|
* {@link ApiError} naming the plugin for the first entry it cannot obtain
|
|
130
|
-
* (`bootstrap.missing_file`, `bootstrap.download_failed`, `bootstrap.integrity_mismatch`)
|
|
153
|
+
* (`bootstrap.missing_file`, `bootstrap.download_failed`, `bootstrap.integrity_mismatch`), except
|
|
154
|
+
* for the `soft` entries (recorded in `failed`, left out of the result).
|
|
131
155
|
*/
|
|
132
156
|
export declare function resolveBootstrapPackages(manifest: BootstrapManifest, o: BootstrapResolveOptions): Promise<Map<string, Uint8Array>>;
|
|
133
157
|
/** The lock around a whole run (`lock:bootstrap`). */
|
|
@@ -181,6 +205,17 @@ export declare class BootstrapService {
|
|
|
181
205
|
dir?: string;
|
|
182
206
|
signal?: AbortSignal;
|
|
183
207
|
carry?: readonly BootstrapEntryResult[];
|
|
208
|
+
/**
|
|
209
|
+
* The first pass of a start (before the extensions load, so no object store): builtin entries
|
|
210
|
+
* are skipped, and the run throws {@link BootstrapFailedError} with them as `deferred` instead
|
|
211
|
+
* of being recorded as done.
|
|
212
|
+
*/
|
|
213
|
+
deferBuiltin?: boolean;
|
|
214
|
+
/**
|
|
215
|
+
* Packages an earlier pass of the same start obtained ({@link BootstrapFailedError.packages});
|
|
216
|
+
* one whose sha512 still equals its entry's pin is used instead of obtaining it again.
|
|
217
|
+
*/
|
|
218
|
+
packages?: ReadonlyMap<string, Uint8Array>;
|
|
184
219
|
/** The plugin import a declarative entry (or an entry's catalog version) goes through. */
|
|
185
220
|
imports: BootstrapImports;
|
|
186
221
|
}): Promise<BootstrapResult>;
|
|
@@ -201,11 +236,15 @@ export declare class BootstrapService {
|
|
|
201
236
|
/** The entry names the last completed run handled (undefined before any run completed). */
|
|
202
237
|
private lastRunNames;
|
|
203
238
|
private entry;
|
|
239
|
+
/** Whether a live (not deleted) platform catalog item has this name. */
|
|
240
|
+
private hasLiveItem;
|
|
204
241
|
/**
|
|
205
|
-
* The catalog item of a bootstrap entry follows `required`:
|
|
206
|
-
* to every org (scope `all
|
|
207
|
-
* disabled); an entry that stops being required
|
|
208
|
-
* admin may change it from then on).
|
|
242
|
+
* The catalog item of a bootstrap entry follows `required` / `builtin`: such an entry is builtin,
|
|
243
|
+
* offered to every org (scope `all`, which an org admin cannot switch off) and not disabled
|
|
244
|
+
* (migration 0075 could leave a required plugin disabled); an entry that stops being required or
|
|
245
|
+
* builtin is no longer builtin and keeps scope `all` (the admin may change it from then on).
|
|
246
|
+
* Only the platform item: an org-owned item of the same name (the entry then fails with
|
|
247
|
+
* `plugin.name_taken`) is never touched.
|
|
209
248
|
*/
|
|
210
249
|
private markBuiltin;
|
|
211
250
|
/** A declarative plugin: catalog only. Returns false when that version is already imported. */
|
|
@@ -57,9 +57,15 @@ import { ExtensionRegistry } from "./registry.js";
|
|
|
57
57
|
* until expiry) aborts the run at its next step. "Done" is a `bootstrap_runs` row for the
|
|
58
58
|
* manifest's sha256 (written with the audit `bootstrap.completed` only when every entry
|
|
59
59
|
* succeeded; otherwise the next start runs it again, which is idempotent).
|
|
60
|
+
* - Two passes per start: the first runs before the extensions load (no object store yet), the
|
|
61
|
+
* second after them. A `builtin` entry (not required, but builtin in the catalog) is deferred by
|
|
62
|
+
* the first pass (`deferBuiltin`: skipped, not a failure; the run is then not recorded and the
|
|
63
|
+
* start runs the second pass), so it may ship skills and templates. A required entry is imported
|
|
64
|
+
* by the first pass and must succeed; a builtin one that fails is logged like an optional one.
|
|
60
65
|
* - Only entries new to the manifest are installed (Ruling 5): an optional entry that the last
|
|
61
66
|
* completed run already handled and that is not installed now (a super admin uninstalled
|
|
62
|
-
* it) stays uninstalled -> `kept`. A required entry is always installed
|
|
67
|
+
* it) stays uninstalled -> `kept`. A required or builtin entry is always installed (a
|
|
68
|
+
* declarative one imported again when its catalog item is gone).
|
|
63
69
|
* - Per entry, by the installed state of its extension:
|
|
64
70
|
* - not installed (and new, or required): imported into the catalog (source `bootstrap`, ref
|
|
65
71
|
* `bootstrap:<name>@<version>`, like a marketplace import), installed, approved (snapshot
|
|
@@ -87,22 +93,35 @@ import { ExtensionRegistry } from "./registry.js";
|
|
|
87
93
|
* never needed again. A plugin's icons and setup guide files are kept in PG too (`catalog_files`),
|
|
88
94
|
* so they never need it either; its skills, connector templates and agent templates do (an entry
|
|
89
95
|
* shipping them fails while no object store runs; an optional one is retried after the extensions
|
|
90
|
-
* loaded
|
|
96
|
+
* loaded, and a plugin that must be builtin and ships them is a `builtin` entry, which only the
|
|
97
|
+
* pass after the extensions load imports).
|
|
91
98
|
*/
|
|
92
99
|
/** The bootstrap manifest that ships with the kernel package. */
|
|
93
100
|
export const BOOTSTRAP_MANIFEST_FILE = fileURLToPath(new URL("../../bootstrap.json", import.meta.url));
|
|
94
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Some entries failed or were deferred (the others were done); the run is not recorded as done.
|
|
103
|
+
*/
|
|
95
104
|
export class BootstrapFailedError extends Error {
|
|
96
105
|
failures;
|
|
97
106
|
results;
|
|
107
|
+
deferred;
|
|
108
|
+
packages;
|
|
98
109
|
/**
|
|
99
110
|
* @param results what the entries that did not fail did (pass them as `carry` to a later run in
|
|
100
111
|
* the same start, so its record keeps `installed` instead of `kept`).
|
|
112
|
+
* @param deferred the builtin entries a first pass skipped (`deferBuiltin`): not failures, the
|
|
113
|
+
* later pass of the same start imports them.
|
|
114
|
+
* @param packages the packages this run obtained, by entry name (pass them as `packages` to the
|
|
115
|
+
* later pass of the same start, so nothing is downloaded twice).
|
|
101
116
|
*/
|
|
102
|
-
constructor(failures, results = []) {
|
|
103
|
-
super(
|
|
117
|
+
constructor(failures, results = [], deferred = [], packages = new Map()) {
|
|
118
|
+
super(failures.length > 0
|
|
119
|
+
? `bootstrap failed for ${failures.map((f) => `${f.name}${f.required ? " (required)" : ""}: ${f.message}`).join("; ")}`
|
|
120
|
+
: `bootstrap deferred the builtin plugins ${deferred.join(", ")} to the pass after the extensions load`);
|
|
104
121
|
this.failures = failures;
|
|
105
122
|
this.results = results;
|
|
123
|
+
this.deferred = deferred;
|
|
124
|
+
this.packages = packages;
|
|
106
125
|
this.name = "BootstrapFailedError";
|
|
107
126
|
}
|
|
108
127
|
/** A required entry failed: the kernel must not serve without it. */
|
|
@@ -128,7 +147,8 @@ const integrityOf = (bytes) => `sha512-${createHash("sha512").update(bytes).dige
|
|
|
128
147
|
* file there has that sha512, else (a `source: "npm"` entry) downloaded from the npm registry and
|
|
129
148
|
* verified against the pin (the packument's `dist.integrity` is not consulted). Throws an
|
|
130
149
|
* {@link ApiError} naming the plugin for the first entry it cannot obtain
|
|
131
|
-
* (`bootstrap.missing_file`, `bootstrap.download_failed`, `bootstrap.integrity_mismatch`)
|
|
150
|
+
* (`bootstrap.missing_file`, `bootstrap.download_failed`, `bootstrap.integrity_mismatch`), except
|
|
151
|
+
* for the `soft` entries (recorded in `failed`, left out of the result).
|
|
132
152
|
*/
|
|
133
153
|
export async function resolveBootstrapPackages(manifest, o) {
|
|
134
154
|
const out = new Map();
|
|
@@ -141,11 +161,20 @@ export async function resolveBootstrapPackages(manifest, o) {
|
|
|
141
161
|
const where = o.dir !== undefined ? o.dir : "the bootstrap directory (config bootstrap.dir)";
|
|
142
162
|
for (const e of manifest.plugins) {
|
|
143
163
|
signal.throwIfAborted();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
out.set(e.name, found);
|
|
147
|
-
continue;
|
|
164
|
+
try {
|
|
165
|
+
out.set(e.name, await obtain(e));
|
|
148
166
|
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
if (signal.aborted || !(err instanceof ApiError) || o.soft?.(e) !== true)
|
|
169
|
+
throw err;
|
|
170
|
+
o.failed?.set(e.name, err);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
async function obtain(e) {
|
|
175
|
+
const found = offline.get(e.integrity);
|
|
176
|
+
if (found !== undefined)
|
|
177
|
+
return found;
|
|
149
178
|
if (e.source === "workspace") {
|
|
150
179
|
throw new ApiError("bootstrap.missing_file", `Bootstrap plugin ${e.name} ${e.version}: no file in ${where} has its pinned sha512 (${e.integrity.slice(0, 19)}…), and a workspace entry cannot be downloaded; put its package tgz (pnpm pack-plugin ${e.path}) in that directory`);
|
|
151
180
|
}
|
|
@@ -153,7 +182,7 @@ export async function resolveBootstrapPackages(manifest, o) {
|
|
|
153
182
|
const offlineHint = `to install offline, put the package tgz (npm pack ${e.npm}@${e.version}, any file name) in ${where}`;
|
|
154
183
|
try {
|
|
155
184
|
const r = await fetchPackage({ registry: npmRegistry, ...(npmToken !== undefined && { token: npmToken }), maxBytes }, e.npm, e.version, signal, { expectedIntegrity: e.integrity });
|
|
156
|
-
|
|
185
|
+
return r.bytes;
|
|
157
186
|
}
|
|
158
187
|
catch (err) {
|
|
159
188
|
if (signal.aborted)
|
|
@@ -169,7 +198,6 @@ export async function resolveBootstrapPackages(manifest, o) {
|
|
|
169
198
|
throw new ApiError("bootstrap.download_failed", `Bootstrap plugin ${e.name} ${e.version}: cannot download ${e.npm}@${e.version} from ${registry} (${reason}); ${offlineHint}, or set packages.npmRegistry to a reachable mirror`);
|
|
170
199
|
}
|
|
171
200
|
}
|
|
172
|
-
return out;
|
|
173
201
|
}
|
|
174
202
|
/** The registry's origin, for messages (never its credentials or path). */
|
|
175
203
|
function registryOrigin(registry) {
|
|
@@ -298,20 +326,47 @@ let BootstrapService = class BootstrapService {
|
|
|
298
326
|
const done = await recorded();
|
|
299
327
|
if (done)
|
|
300
328
|
return done;
|
|
301
|
-
|
|
302
|
-
const
|
|
329
|
+
const deferBuiltin = o.deferBuiltin === true;
|
|
330
|
+
const isDeferred = (e) => deferBuiltin && e.builtin === true;
|
|
331
|
+
// Every package is obtained and checked before the first write (a deferred entry's by the
|
|
332
|
+
// later pass). The first pass refuses to write anything when one is missing or tampered;
|
|
333
|
+
// the later pass only fails that entry unless it is required.
|
|
334
|
+
const files = new Map();
|
|
335
|
+
const fetchFailed = new Map();
|
|
336
|
+
const toObtain = [];
|
|
337
|
+
for (const e of o.manifest.plugins) {
|
|
338
|
+
if (isDeferred(e))
|
|
339
|
+
continue;
|
|
340
|
+
const kept = o.packages?.get(e.name);
|
|
341
|
+
if (kept !== undefined && integrityOf(kept) === e.integrity)
|
|
342
|
+
files.set(e.name, kept);
|
|
343
|
+
else
|
|
344
|
+
toObtain.push(e);
|
|
345
|
+
}
|
|
346
|
+
const obtained = await resolveBootstrapPackages({ ...o.manifest, plugins: toObtain }, {
|
|
303
347
|
...(o.dir !== undefined && { dir: o.dir }),
|
|
304
348
|
config: this.config(),
|
|
305
349
|
signal,
|
|
306
350
|
log: this.log,
|
|
351
|
+
...(!deferBuiltin && { soft: (e) => !e.required, failed: fetchFailed }),
|
|
307
352
|
});
|
|
353
|
+
for (const [name, bytes] of obtained)
|
|
354
|
+
files.set(name, bytes);
|
|
308
355
|
const handled = await this.lastRunNames();
|
|
309
356
|
const results = [];
|
|
310
357
|
const failures = [];
|
|
358
|
+
const deferred = [];
|
|
311
359
|
for (const e of o.manifest.plugins) {
|
|
312
360
|
signal.throwIfAborted();
|
|
361
|
+
if (isDeferred(e)) {
|
|
362
|
+
deferred.push(e.name);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
313
365
|
await this.warnIfHeld(e);
|
|
314
366
|
try {
|
|
367
|
+
const fetchErr = fetchFailed.get(e.name);
|
|
368
|
+
if (fetchErr !== undefined)
|
|
369
|
+
throw fetchErr;
|
|
315
370
|
const r = await this.entry(e, files.get(e.name), handled, o.imports);
|
|
316
371
|
results.push(carried(r, o.carry));
|
|
317
372
|
}
|
|
@@ -320,6 +375,7 @@ let BootstrapService = class BootstrapService {
|
|
|
320
375
|
plugin: e.name,
|
|
321
376
|
version: e.version,
|
|
322
377
|
required: e.required,
|
|
378
|
+
...(e.builtin === true && { builtin: true }),
|
|
323
379
|
err,
|
|
324
380
|
});
|
|
325
381
|
failures.push({
|
|
@@ -331,8 +387,9 @@ let BootstrapService = class BootstrapService {
|
|
|
331
387
|
}
|
|
332
388
|
}
|
|
333
389
|
results.push(...(await this.clearRemoved(o.manifest)));
|
|
334
|
-
if (failures.length > 0)
|
|
335
|
-
throw new BootstrapFailedError(failures, results);
|
|
390
|
+
if (failures.length > 0 || deferred.length > 0) {
|
|
391
|
+
throw new BootstrapFailedError(failures, results, deferred, files);
|
|
392
|
+
}
|
|
336
393
|
signal.throwIfAborted();
|
|
337
394
|
await this.recordDone(manifestSha256, results);
|
|
338
395
|
this.log.info("bootstrap completed", { manifestSha256, results });
|
|
@@ -421,12 +478,15 @@ let BootstrapService = class BootstrapService {
|
|
|
421
478
|
const ref = `bootstrap:${e.name}@${e.version}`;
|
|
422
479
|
// Not new to the manifest: an optional entry left out on purpose stays out (Ruling 5)
|
|
423
480
|
const isNew = handled === undefined || !handled.has(e.name);
|
|
481
|
+
// Required and builtin entries are builtin in the catalog and put back when missing
|
|
482
|
+
const builtin = e.required || e.builtin === true;
|
|
424
483
|
if (!pkg.manifest.extension) {
|
|
425
|
-
await this.markBuiltin(e.name,
|
|
426
|
-
if (!isNew)
|
|
484
|
+
await this.markBuiltin(e.name, builtin);
|
|
485
|
+
if (!isNew && !(builtin && !(await this.hasLiveItem(e.name)))) {
|
|
427
486
|
return { name: e.name, action: "kept", version: e.version };
|
|
487
|
+
}
|
|
428
488
|
const imported = await this.importOnly(e, bytes, ref, imports);
|
|
429
|
-
await this.markBuiltin(e.name,
|
|
489
|
+
await this.markBuiltin(e.name, builtin);
|
|
430
490
|
return { name: e.name, action: imported ? "imported" : "kept", version: e.version };
|
|
431
491
|
}
|
|
432
492
|
if (this.registry.declared().has(e.name)) {
|
|
@@ -438,7 +498,7 @@ let BootstrapService = class BootstrapService {
|
|
|
438
498
|
.select(["version", "source", "required", "desired_state", "sha256"])
|
|
439
499
|
.where("name", "=", e.name)
|
|
440
500
|
.executeTakeFirst();
|
|
441
|
-
if (!row && !isNew && !
|
|
501
|
+
if (!row && !isNew && !builtin) {
|
|
442
502
|
this.log.info("bootstrap: an optional plugin that is not installed stays uninstalled", {
|
|
443
503
|
plugin: e.name,
|
|
444
504
|
});
|
|
@@ -476,18 +536,31 @@ let BootstrapService = class BootstrapService {
|
|
|
476
536
|
if (e.required && row?.desired_state === "disabled") {
|
|
477
537
|
leftDisabled = await this.enableOrWarn(e);
|
|
478
538
|
}
|
|
479
|
-
await this.markBuiltin(e.name,
|
|
539
|
+
await this.markBuiltin(e.name, builtin);
|
|
480
540
|
return leftDisabled === undefined ? result : { ...result, leftDisabled };
|
|
481
541
|
}
|
|
542
|
+
/** Whether a live (not deleted) platform catalog item has this name. */
|
|
543
|
+
async hasLiveItem(name) {
|
|
544
|
+
const row = await this.db
|
|
545
|
+
.selectFrom("catalog_items")
|
|
546
|
+
.select("id")
|
|
547
|
+
.where("name", "=", name)
|
|
548
|
+
.where("owner_org_id", "is", null)
|
|
549
|
+
.where("deleted_at", "is", null)
|
|
550
|
+
.executeTakeFirst();
|
|
551
|
+
return row !== undefined;
|
|
552
|
+
}
|
|
482
553
|
/**
|
|
483
|
-
* The catalog item of a bootstrap entry follows `required`:
|
|
484
|
-
* to every org (scope `all
|
|
485
|
-
* disabled); an entry that stops being required
|
|
486
|
-
* admin may change it from then on).
|
|
554
|
+
* The catalog item of a bootstrap entry follows `required` / `builtin`: such an entry is builtin,
|
|
555
|
+
* offered to every org (scope `all`, which an org admin cannot switch off) and not disabled
|
|
556
|
+
* (migration 0075 could leave a required plugin disabled); an entry that stops being required or
|
|
557
|
+
* builtin is no longer builtin and keeps scope `all` (the admin may change it from then on).
|
|
558
|
+
* Only the platform item: an org-owned item of the same name (the entry then fails with
|
|
559
|
+
* `plugin.name_taken`) is never touched.
|
|
487
560
|
*/
|
|
488
|
-
async markBuiltin(name,
|
|
561
|
+
async markBuiltin(name, builtin) {
|
|
489
562
|
const touch = { updated_at: sql `now()` };
|
|
490
|
-
if (
|
|
563
|
+
if (builtin) {
|
|
491
564
|
await this.db
|
|
492
565
|
.updateTable("catalog_items")
|
|
493
566
|
.set({
|
|
@@ -498,6 +571,7 @@ let BootstrapService = class BootstrapService {
|
|
|
498
571
|
...touch,
|
|
499
572
|
})
|
|
500
573
|
.where("name", "=", name)
|
|
574
|
+
.where("owner_org_id", "is", null)
|
|
501
575
|
.where("deleted_at", "is", null)
|
|
502
576
|
.where((eb) => eb.or([eb("builtin", "=", false), eb("scope", "<>", "all"), eb("disabled", "=", true)]))
|
|
503
577
|
.execute();
|
|
@@ -507,6 +581,7 @@ let BootstrapService = class BootstrapService {
|
|
|
507
581
|
.updateTable("catalog_items")
|
|
508
582
|
.set({ builtin: false, ...touch })
|
|
509
583
|
.where("name", "=", name)
|
|
584
|
+
.where("owner_org_id", "is", null)
|
|
510
585
|
.where("deleted_at", "is", null)
|
|
511
586
|
.where("builtin", "=", true)
|
|
512
587
|
.execute();
|
|
@@ -582,6 +657,7 @@ let BootstrapService = class BootstrapService {
|
|
|
582
657
|
.selectFrom("catalog_items")
|
|
583
658
|
.select("id")
|
|
584
659
|
.where("name", "=", e.name)
|
|
660
|
+
.where("owner_org_id", "is", null)
|
|
585
661
|
.where("deleted_at", "is", null)
|
|
586
662
|
.executeTakeFirst();
|
|
587
663
|
const installed = await this.ops.install(e.name, review.sha256, null, undefined, item ? { catalogItemId: item.id } : {});
|
|
@@ -650,6 +726,7 @@ let BootstrapService = class BootstrapService {
|
|
|
650
726
|
.updateTable("catalog_items")
|
|
651
727
|
.set({ builtin: false, updated_at: sql `now()` })
|
|
652
728
|
.where("builtin", "=", true)
|
|
729
|
+
.where("owner_org_id", "is", null)
|
|
653
730
|
.where("deleted_at", "is", null);
|
|
654
731
|
if (names.length > 0)
|
|
655
732
|
items = items.where("name", "not in", names);
|
|
@@ -99,6 +99,8 @@ export declare class KernelLifecycle implements OnApplicationBootstrap, BeforeAp
|
|
|
99
99
|
private unwatchConfig;
|
|
100
100
|
/** What the first bootstrap pass did, carried into the second one. */
|
|
101
101
|
private bootstrapCarry;
|
|
102
|
+
/** The packages the first bootstrap pass obtained, so the second downloads nothing again. */
|
|
103
|
+
private bootstrapPackages;
|
|
102
104
|
constructor(accounts: AccountsService, configService: KernelConfigService, log: KernelLogger, options: KernelAppOptions, pool: Kysely<any>, redis: Redis, kdb: KernelDb, shutdown: GracefulShutdown, host: ExtensionHost, registry: ExtensionRegistry, installs: ExtensionInstalls, blobs: ExtensionBlobs, extensionLifecycle: ExtensionLifecycle, replicaStatus: ReplicaStatusStore, bootstrap: BootstrapService, marketplaces: MarketplacesService, pluginImport: PluginImport, bus: Pick<ExtensionContext["bus"], "publish" | "subscribe">, usageReport: UsageReporter, titles: TitleQueue, channelJobs: ChannelJobs, channelOutbound: ChannelOutbound, presence: ReplicaPresence, worker?: WorkerRole | undefined, gateway?: GatewayRole | undefined);
|
|
103
105
|
/** The kernel HTTP application (set up by `buildKernelApp` before `init()`). */
|
|
104
106
|
get http(): KernelHttpApp;
|
|
@@ -122,7 +124,9 @@ export declare class KernelLifecycle implements OnApplicationBootstrap, BeforeAp
|
|
|
122
124
|
* kernel never serves without a required plugin. A failed optional entry is logged and returns
|
|
123
125
|
* true: the start runs the step once more after the extensions loaded (a declarative entry's
|
|
124
126
|
* catalog files need the object store, which on a fresh deployment is itself a bootstrap plugin),
|
|
125
|
-
* and the next start tries again while it stays incomplete.
|
|
127
|
+
* and the next start tries again while it stays incomplete. The first pass skips the `builtin`
|
|
128
|
+
* entries (they may ship skills and templates, which need the object store) and returns true for
|
|
129
|
+
* them too; the final pass imports them, and one that fails is logged like an optional entry.
|
|
126
130
|
*/
|
|
127
131
|
private runBootstrap;
|
|
128
132
|
/** 部署尚无活跃超级管理员时提示去初始化;查询失败只记日志,不影响启动。 */
|
|
@@ -120,6 +120,8 @@ let KernelLifecycle = class KernelLifecycle {
|
|
|
120
120
|
unwatchConfig;
|
|
121
121
|
/** What the first bootstrap pass did, carried into the second one. */
|
|
122
122
|
bootstrapCarry;
|
|
123
|
+
/** The packages the first bootstrap pass obtained, so the second downloads nothing again. */
|
|
124
|
+
bootstrapPackages;
|
|
123
125
|
constructor(accounts, configService, log, options,
|
|
124
126
|
// biome-ignore lint/suspicious/noExplicitAny: the pool carries no schema types
|
|
125
127
|
pool, redis, kdb, shutdown, host, registry, installs, blobs, extensionLifecycle, replicaStatus, bootstrap, marketplaces, pluginImport, bus,
|
|
@@ -303,7 +305,9 @@ let KernelLifecycle = class KernelLifecycle {
|
|
|
303
305
|
* kernel never serves without a required plugin. A failed optional entry is logged and returns
|
|
304
306
|
* true: the start runs the step once more after the extensions loaded (a declarative entry's
|
|
305
307
|
* catalog files need the object store, which on a fresh deployment is itself a bootstrap plugin),
|
|
306
|
-
* and the next start tries again while it stays incomplete.
|
|
308
|
+
* and the next start tries again while it stays incomplete. The first pass skips the `builtin`
|
|
309
|
+
* entries (they may ship skills and templates, which need the object store) and returns true for
|
|
310
|
+
* them too; the final pass imports them, and one that fails is logged like an optional entry.
|
|
307
311
|
*/
|
|
308
312
|
async runBootstrap(dir, final = false) {
|
|
309
313
|
const manifest = this.options.bootstrapManifest ?? (await readBootstrapManifest());
|
|
@@ -318,17 +322,25 @@ let KernelLifecycle = class KernelLifecycle {
|
|
|
318
322
|
...(dir !== undefined && { dir }),
|
|
319
323
|
signal: this.shutdown.signal,
|
|
320
324
|
...(this.bootstrapCarry !== undefined && { carry: this.bootstrapCarry }),
|
|
325
|
+
...(this.bootstrapPackages !== undefined && { packages: this.bootstrapPackages }),
|
|
326
|
+
deferBuiltin: !final,
|
|
321
327
|
});
|
|
322
328
|
return false;
|
|
323
329
|
}
|
|
324
330
|
catch (e) {
|
|
325
331
|
if (e instanceof BootstrapFailedError && !e.requiredFailed) {
|
|
326
332
|
this.bootstrapCarry = e.results;
|
|
327
|
-
|
|
328
|
-
|
|
333
|
+
this.bootstrapPackages = e.packages;
|
|
334
|
+
if (e.failures.length === 0) {
|
|
335
|
+
this.log.info("bootstrap: builtin plugins are installed once the extensions have loaded", {
|
|
336
|
+
deferred: e.deferred,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
else if (final) {
|
|
340
|
+
this.log.error("bootstrap: optional or builtin plugins could not be installed; retried on the next start", { failures: e.failures });
|
|
329
341
|
}
|
|
330
342
|
else {
|
|
331
|
-
this.log.warn("bootstrap: optional plugins could not be installed yet; retried once the extensions have loaded", { failures: e.failures });
|
|
343
|
+
this.log.warn("bootstrap: optional or builtin plugins could not be installed yet; retried once the extensions have loaded", { failures: e.failures });
|
|
332
344
|
}
|
|
333
345
|
return true;
|
|
334
346
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capybara-hq/kernel",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.21",
|
|
4
4
|
"description": "Capybara kernel: config, database, plugin host and HTTP API of the Capybara agent platform",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"yaml": "^2.9.1",
|
|
66
66
|
"yauzl": "3.4.0",
|
|
67
67
|
"zod": "^4.6.7",
|
|
68
|
-
"@capybara-hq/plugin-sdk": "^0.1.0-rc.
|
|
68
|
+
"@capybara-hq/plugin-sdk": "^0.1.0-rc.15"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@langchain/langgraph-checkpoint-validation": "^1.2.0",
|