@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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "schemaVersion": 1,
4
4
  "marketplace": {
5
5
  "url": "https://github.com/capybara-hq/plugins.git",
6
- "commit": "14c73d5c51415223326d89414d62b7278ad60745"
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.14";
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
- /** Some entries failed (the others were done); the run is not recorded as done. */
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`: a required entry is builtin, offered
206
- * to every org (scope `all`) and not disabled (migration 0075 could leave a required plugin
207
- * disabled); an entry that stops being required is no longer builtin and keeps scope `all` (the
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
- /** Some entries failed (the others were done); the run is not recorded as done. */
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(`bootstrap failed for ${failures.map((f) => `${f.name}${f.required ? " (required)" : ""}: ${f.message}`).join("; ")}`);
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
- const found = offline.get(e.integrity);
145
- if (found !== undefined) {
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
- out.set(e.name, r.bytes);
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
- // Every package is obtained and checked before the first write
302
- const files = await resolveBootstrapPackages(o.manifest, {
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, e.required);
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, e.required);
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 && !e.required) {
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, e.required);
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`: a required entry is builtin, offered
484
- * to every org (scope `all`) and not disabled (migration 0075 could leave a required plugin
485
- * disabled); an entry that stops being required is no longer builtin and keeps scope `all` (the
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, required) {
561
+ async markBuiltin(name, builtin) {
489
562
  const touch = { updated_at: sql `now()` };
490
- if (required) {
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
- if (final) {
328
- this.log.error("bootstrap: optional plugins could not be installed; retried on the next start", { failures: e.failures });
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.20",
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.14"
68
+ "@capybara-hq/plugin-sdk": "^0.1.0-rc.15"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@langchain/langgraph-checkpoint-validation": "^1.2.0",