@capybara-hq/kernel 0.1.0-rc.24 → 0.1.0-rc.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/dist/admin/admin.module.js +2 -0
  2. package/dist/admin/health.service.d.ts +2 -2
  3. package/dist/admin/health.service.js +6 -2
  4. package/dist/agents/agents.module.js +3 -0
  5. package/dist/agents/agents.service.d.ts +6 -1
  6. package/dist/agents/agents.service.js +11 -2
  7. package/dist/catalog/catalog-import.module.js +0 -4
  8. package/dist/catalog/catalog-scope.service.d.ts +63 -26
  9. package/dist/catalog/catalog-scope.service.js +146 -71
  10. package/dist/catalog/catalog.module.js +4 -3
  11. package/dist/catalog/catalog.service.d.ts +11 -4
  12. package/dist/catalog/catalog.service.js +21 -5
  13. package/dist/catalog/cli.d.ts +4 -3
  14. package/dist/catalog/cli.js +8 -13
  15. package/dist/catalog/job-ids.d.ts +8 -20
  16. package/dist/catalog/job-ids.js +11 -16
  17. package/dist/catalog/marketplaces.service.d.ts +1 -1
  18. package/dist/catalog/org-plugin-uploads.d.ts +11 -4
  19. package/dist/catalog/org-plugin-uploads.js +21 -6
  20. package/dist/catalog/owned-items.d.ts +6 -13
  21. package/dist/catalog/owned-items.js +11 -29
  22. package/dist/catalog/owned-plugin-cleanup.d.ts +8 -17
  23. package/dist/catalog/owned-plugin-cleanup.js +12 -66
  24. package/dist/catalog/platform-components.d.ts +56 -34
  25. package/dist/catalog/platform-components.js +68 -78
  26. package/dist/catalog/plugin-import.d.ts +28 -25
  27. package/dist/catalog/plugin-import.js +90 -76
  28. package/dist/catalog/plugin-upgrade.d.ts +4 -4
  29. package/dist/catalog/plugin-upgrade.js +7 -19
  30. package/dist/catalog/scope-jobs.d.ts +59 -0
  31. package/dist/catalog/scope-jobs.js +65 -0
  32. package/dist/catalog/scope-sync-workers.d.ts +22 -0
  33. package/dist/catalog/scope-sync-workers.js +58 -2
  34. package/dist/catalog/scope-sync.d.ts +40 -65
  35. package/dist/catalog/scope-sync.js +114 -122
  36. package/dist/channels/inbound.js +2 -2
  37. package/dist/channels/threads.d.ts +4 -3
  38. package/dist/channels/threads.js +4 -3
  39. package/dist/conversations/conversations.controller.js +4 -2
  40. package/dist/conversations/conversations.service.d.ts +17 -5
  41. package/dist/conversations/conversations.service.js +17 -5
  42. package/dist/conversations/events.d.ts +21 -0
  43. package/dist/conversations/events.js +24 -1
  44. package/dist/conversations/source.d.ts +10 -0
  45. package/dist/conversations/source.js +17 -0
  46. package/dist/conversations/tree.d.ts +10 -0
  47. package/dist/conversations/tree.js +35 -0
  48. package/dist/db/kernel-schema.d.ts +9 -14
  49. package/dist/devices/devices.service.d.ts +11 -8
  50. package/dist/devices/devices.service.js +16 -23
  51. package/dist/evals/run-replay.d.ts +13 -2
  52. package/dist/evals/run-replay.js +28 -1
  53. package/dist/evals/snapshot.d.ts +7 -1
  54. package/dist/evals/snapshot.js +2 -1
  55. package/dist/extensions/hooks.d.ts +22 -3
  56. package/dist/extensions/hooks.js +15 -1
  57. package/dist/extensions/lifecycle-hooks.d.ts +99 -0
  58. package/dist/extensions/lifecycle-hooks.js +273 -0
  59. package/dist/extensions/org-config.d.ts +13 -1
  60. package/dist/extensions/org-config.js +19 -2
  61. package/dist/extensions/permission-view.d.ts +9 -5
  62. package/dist/extensions/permission-view.js +9 -4
  63. package/dist/extensions/point-defaults.d.ts +28 -1
  64. package/dist/extensions/point-defaults.js +44 -13
  65. package/dist/inbox/inbox.service.d.ts +6 -20
  66. package/dist/inbox/inbox.service.js +0 -3
  67. package/dist/inbox/org-notices.d.ts +1 -1
  68. package/dist/inbox/platform-inbox.service.d.ts +3 -32
  69. package/dist/inbox/platform-inbox.service.js +5 -54
  70. package/dist/infra/outbox/registry.d.ts +124 -0
  71. package/dist/infra/outbox/registry.js +126 -0
  72. package/dist/infra/queue/queue-names.d.ts +1 -1
  73. package/dist/infra/queue/queue-names.js +1 -0
  74. package/dist/orgs/directory-sync.js +1 -1
  75. package/dist/orgs/members.service.js +1 -1
  76. package/dist/orgs/offboarding.d.ts +50 -57
  77. package/dist/orgs/offboarding.js +117 -162
  78. package/dist/orgs/orgs.module.js +3 -0
  79. package/dist/orgs/orgs.service.d.ts +11 -5
  80. package/dist/orgs/orgs.service.js +15 -6
  81. package/dist/overrides.d.ts +3 -0
  82. package/dist/overrides.js +2 -0
  83. package/dist/platform/ops/inbox.ops.js +4 -20
  84. package/dist/platform/ops/platform.ops.d.ts +1 -0
  85. package/dist/platform/ops/platform.ops.js +6 -3
  86. package/dist/roles/worker-role.d.ts +10 -5
  87. package/dist/roles/worker-role.js +19 -9
  88. package/dist/roles/worker.module.js +2 -4
  89. package/dist/runtime/context/build.d.ts +3 -0
  90. package/dist/runtime/context/build.js +15 -0
  91. package/dist/runtime/context/plugin-context.d.ts +20 -0
  92. package/dist/runtime/context/plugin-context.js +30 -0
  93. package/dist/runtime/langgraph-loop.js +4 -0
  94. package/dist/runtime/loop.d.ts +8 -0
  95. package/dist/runtime/maintenance.d.ts +5 -3
  96. package/dist/runtime/maintenance.js +36 -11
  97. package/dist/runtime/model-call.d.ts +6 -1
  98. package/dist/runtime/model-call.js +1 -0
  99. package/dist/runtime/plugin-context.service.d.ts +69 -0
  100. package/dist/runtime/plugin-context.service.js +208 -0
  101. package/dist/runtime/run-control.d.ts +6 -1
  102. package/dist/runtime/run-control.js +13 -0
  103. package/dist/runtime/run-queue.d.ts +13 -0
  104. package/dist/runtime/run-queue.js +63 -0
  105. package/dist/runtime/run-worker.d.ts +22 -1
  106. package/dist/runtime/run-worker.js +107 -10
  107. package/dist/runtime/runtime.module.js +2 -0
  108. package/dist/runtime/subagents/dispatch.js +1 -0
  109. package/dist/runtime/subagents/skill-fork.js +3 -0
  110. package/dist/sandbox/jobs.d.ts +20 -10
  111. package/dist/sandbox/jobs.js +23 -17
  112. package/dist/schedules/fire.d.ts +25 -4
  113. package/dist/schedules/fire.js +87 -13
  114. package/dist/schedules/job-ids.d.ts +66 -17
  115. package/dist/schedules/job-ids.js +81 -13
  116. package/dist/schedules/run-tracker.js +2 -1
  117. package/dist/schedules/scan.job.d.ts +13 -32
  118. package/dist/schedules/scan.job.js +35 -144
  119. package/dist/schedules/scheduled-tasks.service.d.ts +6 -6
  120. package/dist/schedules/scheduled-tasks.service.js +17 -14
  121. package/dist/schedules/schedules.module.js +0 -4
  122. package/dist/skills/changes.controller.d.ts +1 -1
  123. package/dist/skills/changes.controller.js +1 -1
  124. package/dist/tools/approvals.service.d.ts +15 -4
  125. package/dist/tools/approvals.service.js +26 -10
  126. package/dist/tools/pipeline.d.ts +11 -2
  127. package/dist/tools/pipeline.js +5 -0
  128. package/dist/tools/questions.service.d.ts +15 -4
  129. package/dist/tools/questions.service.js +21 -19
  130. package/dist/tools/registry.d.ts +8 -1
  131. package/dist/tools/registry.js +9 -0
  132. package/package.json +3 -3
  133. package/src/migrations/0121_drop_platform_migration_runs.sql +12 -0
  134. package/src/migrations/0122_drop_scheduled_task_runs_rechecked_at.sql +5 -0
  135. package/src/migrations/0123_pgvector.sql +27 -0
  136. package/src/migrations/0124_conversation_source.sql +36 -0
  137. package/dist/catalog/platform-migration.d.ts +0 -115
  138. package/dist/catalog/platform-migration.js +0 -583
  139. package/dist/infra/queue/add-job-once.d.ts +0 -14
  140. package/dist/infra/queue/add-job-once.js +0 -16
@@ -1,4 +1,4 @@
1
- import { type AgentTemplate, type ConnectorTemplateFile, type ParsedTheme } from "@capybara-hq/plugin-sdk";
1
+ import { type AgentTemplate, type ConnectorTemplateFile, type ParsedTheme, type PluginManifest } from "@capybara-hq/plugin-sdk";
2
2
  import type { SkillManifest, SourceFormat } from "../db/kernel-schema.js";
3
3
  import { KernelDb } from "../infra/database/kernel-db.js";
4
4
  import { TransactionHost } from "../infra/database/transaction-host.js";
@@ -26,7 +26,7 @@ export interface ChangedConnectorTemplateRef {
26
26
  from: string | null;
27
27
  to: string;
28
28
  }
29
- /** What {@link PlatformComponents.syncFromVersion} changed (unchanged components are not listed). */
29
+ /** What {@link PlatformComponents.applySyncIn} changed (unchanged components are not listed). */
30
30
  export interface PlatformSyncResult {
31
31
  skills: ChangedRef[];
32
32
  agentTemplates: ChangedRef[];
@@ -37,11 +37,6 @@ export interface PlatformSyncResult {
37
37
  export declare const platformSkillPrefix: (id: string, version: number) => string;
38
38
  /** The prefix (in `deployment/`) of a platform agent template version's file. */
39
39
  export declare const platformAgentTemplatePrefix: (id: string, version: number) => string;
40
- /**
41
- * The prefix (in `deployment/`) of a platform theme's files (themes spec §6); a version's fonts
42
- * and logo are under `<prefix><version>-<catalog version>/` ({@link syncVersionPrefix}).
43
- */
44
- export declare const platformThemePrefix: (id: string) => string;
45
40
  /**
46
41
  * A fresh prefix (in `deployment/`) for a version written before its transaction (ruling P3-45):
47
42
  * `<id>/<version>-<uuid>/`, so two writers racing for one version number never overwrite each
@@ -49,12 +44,20 @@ export declare const platformThemePrefix: (id: string) => string;
49
44
  */
50
45
  export declare const uniqueVersionPrefix: (base: string) => string;
51
46
  /**
52
- * The prefix of a version a plugin sync writes: `<id>/<version>-<catalog version>/` (the catalog
53
- * version made key-safe), so syncs of two catalog versions racing for one number never overwrite
54
- * each other. Stored on the version row; readers use the stored prefix, so rows written before keep
47
+ * The prefix (in `deployment/`) of the files a plugin sync writes for a component version:
48
+ * `platform/<kind>/sha256-<digest>/`, the digest of the files' paths and contents
49
+ * ({@link skillFilesDigest}). The files are copied before the transaction that writes the rows (no
50
+ * storage call holds a row lock), so the same content always lands on the same keys: a sync or an
51
+ * import that is retried after its transaction rolled back writes nothing new, and versions with
52
+ * the same content share their objects. Objects are immutable and nothing deletes them (versions
53
+ * are kept), so an import that rolled back leaves at most objects a later import of that content
54
+ * reuses. Stored on the version row; readers use the stored prefix, so rows written before keep
55
55
  * theirs.
56
56
  */
57
- export declare const syncVersionPrefix: (base: string, catalogVersion: string) => string;
57
+ export declare const contentPrefix: (kind: "skills" | "agent-templates" | "themes", files: {
58
+ path: string;
59
+ sha256: string;
60
+ }[]) => string;
58
61
  /** The live component a plan was made against (absent: the plan creates it). */
59
62
  interface Base<V> {
60
63
  id: string;
@@ -112,12 +115,32 @@ export interface SyncPlan {
112
115
  export declare class PlanStale extends Error {
113
116
  }
114
117
  /**
115
- * A sync planned and its files copied ({@link PlatformComponents.prepareSync}), waiting for its rows
116
- * ({@link PlatformComponents.applySyncIn}). Opaque outside this module.
118
+ * A sync planned and its files copied ({@link PlatformComponents.prepareSync},
119
+ * {@link PlatformComponents.prepareImport}), waiting for its rows
120
+ * ({@link PlatformComponents.applySyncIn}). Opaque outside this module but for the item and the
121
+ * catalog version it was planned for.
117
122
  */
118
123
  export interface PreparedSync {
119
124
  readonly plan: SyncPlan;
120
125
  }
126
+ /**
127
+ * The catalog version a sync plans from: its row, or a staged import whose row its transaction
128
+ * is about to write (the files are in object storage under `objectPrefix` already).
129
+ */
130
+ export interface SyncSource {
131
+ itemId: string;
132
+ /** The plugin (the catalog item's name). */
133
+ plugin: string;
134
+ version: string;
135
+ manifest: PluginManifest;
136
+ objectPrefix: string | null;
137
+ files: {
138
+ path: string;
139
+ size: number;
140
+ sha256: string;
141
+ }[];
142
+ sourceFormat: SourceFormat;
143
+ }
121
144
  /** A component version written by an upload ({@link PlatformComponents.uploadSkillVersion}). */
122
145
  export interface UploadedComponentVersion {
123
146
  kind: "skill" | "agent_template";
@@ -145,8 +168,9 @@ export interface UploadActor {
145
168
  auditDetail?: Record<string, unknown>;
146
169
  }
147
170
  /**
148
- * Writes the platform rows of a catalog version's components. Called by the import confirm that
149
- * creates a catalog item (`initial`), and by the platform plugin update for a new version.
171
+ * Writes the platform rows of a catalog version's components, in the transaction of the import
172
+ * confirm that makes an item's first usable version, of the super admin's confirmation of an org
173
+ * upload's first version, and of the platform plugin update to a new version.
150
174
  */
151
175
  export declare class PlatformComponents {
152
176
  private readonly kdb;
@@ -160,26 +184,19 @@ export declare class PlatformComponents {
160
184
  /** CLI only (see `PluginImport.useCliStorage`): files go to the object store the CLI set up. */
161
185
  useCliStorage(storage: PrefixedStorage): void;
162
186
  /**
163
- * Creates or updates the platform rows of every component of catalog version `version` of item
164
- * `itemId`: a component is matched by plugin and name (connector templates by item and template
165
- * id) and gets a new version only when its content changed (skills by their files' digest,
166
- * templates by canonical JSON); new rows follow the plugin's scope (`scope_mode: follow`).
167
- * Files are copied from the catalog prefix to `deployment/platform/…` before the rows are
168
- * written (outside the transaction), so a sync that stops midway leaves at most unreferenced
169
- * files, and running it again is a no-op once it succeeded. `initial` is the confirm that
170
- * created the item. Components missing from the version are left alone (the platform update
171
- * decides what a removal means). On success the item records `version` as
172
- * `platform_synced_version`.
187
+ * The first phase of a sync inside a caller's transaction (the platform plugin update, the super
188
+ * admin's confirmation of an org upload's first version: `confirming`, the item still awaiting
189
+ * it): plans the version's components and copies their files, outside any transaction (no
190
+ * storage call holds a row lock). {@link applySyncIn} writes the rows.
173
191
  */
174
- syncFromVersion(itemId: string, version: string, opts: {
175
- initial: boolean;
176
- }): Promise<PlatformSyncResult>;
192
+ prepareSync(itemId: string, version: string, opts?: {
193
+ confirming?: boolean;
194
+ }): Promise<PreparedSync>;
177
195
  /**
178
- * The first phase of a sync inside a caller's transaction (the platform plugin update): plans the
179
- * version's components and copies their files, outside any transaction (no storage call holds a
180
- * row lock). {@link applySyncIn} writes the rows.
196
+ * {@link prepareSync} for an import whose transaction writes the catalog version (the item may
197
+ * not exist yet): plans from the staged content and copies the files, before that transaction.
181
198
  */
182
- prepareSync(itemId: string, version: string): Promise<PreparedSync>;
199
+ prepareImport(source: SyncSource): Promise<PreparedSync>;
183
200
  /**
184
201
  * The second phase: writes the prepared rows in the caller's transaction and records
185
202
  * `platform_synced_version`. A component changed since the plan -> {@link PlanStale} (the caller
@@ -226,11 +243,16 @@ export declare class PlatformComponents {
226
243
  private writeTemplateVersionIn;
227
244
  /**
228
245
  * Records that the item's platform components are written from `version`
229
- * (`catalog_items.platform_synced_version`): the migration job syncs only items without it.
246
+ * (`catalog_items.platform_synced_version`, the version orgs install).
230
247
  */
231
248
  private markSynced;
249
+ /**
250
+ * The stored catalog version `version` of live item `itemId`. An org upload still waiting for the
251
+ * super admin has no platform components (P3-9) unless this is its `confirming`.
252
+ */
253
+ private source;
232
254
  /** Reads the version's components and the live platform rows; keeps only what changed. */
233
- private plan;
255
+ private planFrom;
234
256
  /** Copies the planned versions' files to their platform prefixes. */
235
257
  private writeFiles;
236
258
  /**
@@ -9,8 +9,10 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
9
9
  };
10
10
  // Platform components (plugin scope phase 2, rulings A2 and C2): the skills, agent templates,
11
11
  // connector templates and themes of a catalog item, stored once for the deployment. Files live
12
- // under `deployment/platform/{skills,agent-templates,themes}/<id>/<version>…/`, never under an org
13
- // prefix (orgs are purged by prefix).
12
+ // under `deployment/platform/{skills,agent-templates,themes}/…/`, never under an org prefix (orgs
13
+ // are purged by prefix): a plugin sync's under the digest of their content
14
+ // (`sha256-<digest>/`, {@link contentPrefix}), an upload's or a promotion's under
15
+ // `<id>/<version>-<uuid>/`.
14
16
  import { createHash, randomUUID } from "node:crypto";
15
17
  import { parseAgentTemplate, } from "@capybara-hq/plugin-sdk";
16
18
  import { Inject, Injectable } from "@nestjs/common";
@@ -32,11 +34,6 @@ import { assertNotAwaiting } from "./ownership.js";
32
34
  export const platformSkillPrefix = (id, version) => `platform/skills/${id}/${version}/`;
33
35
  /** The prefix (in `deployment/`) of a platform agent template version's file. */
34
36
  export const platformAgentTemplatePrefix = (id, version) => `platform/agent-templates/${id}/${version}/`;
35
- /**
36
- * The prefix (in `deployment/`) of a platform theme's files (themes spec §6); a version's fonts
37
- * and logo are under `<prefix><version>-<catalog version>/` ({@link syncVersionPrefix}).
38
- */
39
- export const platformThemePrefix = (id) => `platform/themes/${id}/`;
40
37
  /**
41
38
  * A fresh prefix (in `deployment/`) for a version written before its transaction (ruling P3-45):
42
39
  * `<id>/<version>-<uuid>/`, so two writers racing for one version number never overwrite each
@@ -44,22 +41,26 @@ export const platformThemePrefix = (id) => `platform/themes/${id}/`;
44
41
  */
45
42
  export const uniqueVersionPrefix = (base) => `${base.slice(0, -1)}-${randomUUID()}/`;
46
43
  /**
47
- * The prefix of a version a plugin sync writes: `<id>/<version>-<catalog version>/` (the catalog
48
- * version made key-safe), so syncs of two catalog versions racing for one number never overwrite
49
- * each other. Stored on the version row; readers use the stored prefix, so rows written before keep
44
+ * The prefix (in `deployment/`) of the files a plugin sync writes for a component version:
45
+ * `platform/<kind>/sha256-<digest>/`, the digest of the files' paths and contents
46
+ * ({@link skillFilesDigest}). The files are copied before the transaction that writes the rows (no
47
+ * storage call holds a row lock), so the same content always lands on the same keys: a sync or an
48
+ * import that is retried after its transaction rolled back writes nothing new, and versions with
49
+ * the same content share their objects. Objects are immutable and nothing deletes them (versions
50
+ * are kept), so an import that rolled back leaves at most objects a later import of that content
51
+ * reuses. Stored on the version row; readers use the stored prefix, so rows written before keep
50
52
  * theirs.
51
53
  */
52
- export const syncVersionPrefix = (base, catalogVersion) => `${base.slice(0, -1)}-${catalogVersion.replace(/[^0-9A-Za-z.-]/g, "_")}/`;
54
+ export const contentPrefix = (kind, files) => `platform/${kind}/sha256-${skillFilesDigest(files)}/`;
53
55
  /** 409 `catalog.not_owned`: an org's own promoted item is versioned by its owner only (P3-48). */
54
56
  const ownedByOrg = (what, name) => new ApiError("catalog.not_owned", `The ${what} ${name} belongs to an organization that shared it: only that organization uploads its versions`);
55
57
  /** A component changed between the plan and its write (a concurrent sync): plan again. */
56
58
  export class PlanStale extends Error {
57
59
  }
58
- /** Plans are retried this many times against concurrent syncs of the same item. */
59
- const SYNC_ATTEMPTS = 3;
60
60
  /**
61
- * Writes the platform rows of a catalog version's components. Called by the import confirm that
62
- * creates a catalog item (`initial`), and by the platform plugin update for a new version.
61
+ * Writes the platform rows of a catalog version's components, in the transaction of the import
62
+ * confirm that makes an item's first usable version, of the super admin's confirmation of an org
63
+ * upload's first version, and of the platform plugin update to a new version.
63
64
  */
64
65
  let PlatformComponents = class PlatformComponents {
65
66
  kdb;
@@ -82,56 +83,30 @@ let PlatformComponents = class PlatformComponents {
82
83
  useCliStorage(storage) {
83
84
  this.files = storage;
84
85
  }
86
+ // A plugin sync matches a component by plugin and name (connector templates by item and template
87
+ // id) and gives it a new version only when its content changed (skills by their files' digest,
88
+ // templates by canonical JSON); new rows follow the plugin's scope (`scope_mode: follow`).
89
+ // Components missing from the version are left alone (the platform update decides what a removal
90
+ // means). It runs in two phases: {@link prepareImport} / {@link prepareSync} plan and copy the
91
+ // files (keyed by content, {@link contentPrefix}) before the caller's transaction, and
92
+ // {@link applySyncIn} writes the rows and `platform_synced_version` in it.
85
93
  /**
86
- * Creates or updates the platform rows of every component of catalog version `version` of item
87
- * `itemId`: a component is matched by plugin and name (connector templates by item and template
88
- * id) and gets a new version only when its content changed (skills by their files' digest,
89
- * templates by canonical JSON); new rows follow the plugin's scope (`scope_mode: follow`).
90
- * Files are copied from the catalog prefix to `deployment/platform/…` before the rows are
91
- * written (outside the transaction), so a sync that stops midway leaves at most unreferenced
92
- * files, and running it again is a no-op once it succeeded. `initial` is the confirm that
93
- * created the item. Components missing from the version are left alone (the platform update
94
- * decides what a removal means). On success the item records `version` as
95
- * `platform_synced_version`.
94
+ * The first phase of a sync inside a caller's transaction (the platform plugin update, the super
95
+ * admin's confirmation of an org upload's first version: `confirming`, the item still awaiting
96
+ * it): plans the version's components and copies their files, outside any transaction (no
97
+ * storage call holds a row lock). {@link applySyncIn} writes the rows.
96
98
  */
97
- async syncFromVersion(itemId, version, opts) {
98
- for (let attempt = 1;; attempt++) {
99
- const plan = await this.plan(itemId, version);
100
- if (plan.skills.length === 0 &&
101
- plan.agentTemplates.length === 0 &&
102
- plan.connectorTemplates.length === 0 &&
103
- plan.themes.length === 0) {
104
- await this.markSynced(itemId, version);
105
- return { skills: [], agentTemplates: [], connectorTemplates: [], themes: [] };
106
- }
107
- await this.writeFiles(plan);
108
- try {
109
- const r = await this.apply(plan);
110
- await this.markSynced(itemId, version);
111
- this.log.info("platform components synced", {
112
- itemId,
113
- version,
114
- initial: opts.initial,
115
- skills: r.skills.length,
116
- agentTemplates: r.agentTemplates.length,
117
- connectorTemplates: r.connectorTemplates.length,
118
- themes: r.themes.length,
119
- });
120
- return r;
121
- }
122
- catch (e) {
123
- if (!(e instanceof PlanStale) || attempt >= SYNC_ATTEMPTS)
124
- throw e;
125
- }
126
- }
99
+ async prepareSync(itemId, version, opts = {}) {
100
+ const plan = await this.planFrom(await this.source(itemId, version, opts.confirming === true));
101
+ await this.writeFiles(plan);
102
+ return { plan };
127
103
  }
128
104
  /**
129
- * The first phase of a sync inside a caller's transaction (the platform plugin update): plans the
130
- * version's components and copies their files, outside any transaction (no storage call holds a
131
- * row lock). {@link applySyncIn} writes the rows.
105
+ * {@link prepareSync} for an import whose transaction writes the catalog version (the item may
106
+ * not exist yet): plans from the staged content and copies the files, before that transaction.
132
107
  */
133
- async prepareSync(itemId, version) {
134
- const plan = await this.plan(itemId, version);
108
+ async prepareImport(source) {
109
+ const plan = await this.planFrom(source);
135
110
  await this.writeFiles(plan);
136
111
  return { plan };
137
112
  }
@@ -411,7 +386,7 @@ let PlatformComponents = class PlatformComponents {
411
386
  }
412
387
  /**
413
388
  * Records that the item's platform components are written from `version`
414
- * (`catalog_items.platform_synced_version`): the migration job syncs only items without it.
389
+ * (`catalog_items.platform_synced_version`, the version orgs install).
415
390
  */
416
391
  async markSynced(itemId, version) {
417
392
  await this.db
@@ -420,16 +395,18 @@ let PlatformComponents = class PlatformComponents {
420
395
  .where("id", "=", itemId)
421
396
  .execute();
422
397
  }
423
- /** Reads the version's components and the live platform rows; keeps only what changed. */
424
- async plan(itemId, version) {
398
+ /**
399
+ * The stored catalog version `version` of live item `itemId`. An org upload still waiting for the
400
+ * super admin has no platform components (P3-9) unless this is its `confirming`.
401
+ */
402
+ async source(itemId, version, confirming) {
425
403
  const item = await this.db
426
404
  .selectFrom("catalog_items")
427
405
  .select(["name", "awaiting_confirmation"])
428
406
  .where("id", "=", itemId)
429
407
  .where("deleted_at", "is", null)
430
408
  .executeTakeFirst();
431
- // An org upload still waiting for the super admin has no platform components (P3-9)
432
- if (item !== undefined)
409
+ if (item !== undefined && !confirming)
433
410
  assertNotAwaiting(item);
434
411
  const row = item
435
412
  ? await this.db
@@ -442,21 +419,36 @@ let PlatformComponents = class PlatformComponents {
442
419
  if (!item || !row) {
443
420
  throw new ApiError("catalog.item_not_found", `Catalog item ${itemId}@${version} not found`);
444
421
  }
445
- const plugin = item.name;
446
- const manifest = row.manifest;
447
- const v = row;
422
+ return {
423
+ itemId,
424
+ plugin: item.name,
425
+ version,
426
+ manifest: row.manifest,
427
+ objectPrefix: row.object_prefix,
428
+ files: row.files,
429
+ sourceFormat: row.source_format,
430
+ };
431
+ }
432
+ /** Reads the version's components and the live platform rows; keeps only what changed. */
433
+ async planFrom(src) {
434
+ const { itemId, plugin, version, manifest } = src;
435
+ const v = {
436
+ manifest,
437
+ object_prefix: src.objectPrefix,
438
+ files: src.files,
439
+ };
448
440
  const plan = {
449
441
  itemId,
450
442
  plugin,
451
443
  version,
452
- sourceFormat: row.source_format,
444
+ sourceFormat: src.sourceFormat,
453
445
  skills: [],
454
446
  agentTemplates: [],
455
447
  connectorTemplates: [],
456
448
  themes: [],
457
449
  };
458
450
  for (const s of manifest.skills ?? []) {
459
- const files = await readCatalogFiles(this.files, row.object_prefix, row.files, packagePath(s.path));
451
+ const files = await readCatalogFiles(this.files, v.object_prefix, v.files, packagePath(s.path));
460
452
  const pkg = { ...skillOf(files), files };
461
453
  const live = await this.db
462
454
  .selectFrom("platform_skills as p")
@@ -475,8 +467,7 @@ let PlatformComponents = class PlatformComponents {
475
467
  base: live && { id: live.id, version: live.current_version },
476
468
  id,
477
469
  to,
478
- // the catalog version in the prefix: two syncs of different versions never write one key
479
- prefix: syncVersionPrefix(platformSkillPrefix(id, to), version),
470
+ prefix: contentPrefix("skills", files),
480
471
  files,
481
472
  manifest: skillManifest(pkg),
482
473
  });
@@ -495,18 +486,19 @@ let PlatformComponents = class PlatformComponents {
495
486
  .executeTakeFirst();
496
487
  if (live && canonicalJson(live.definition) === canonicalJson(a.template))
497
488
  continue;
498
- const [file] = await readCatalogFiles(this.files, row.object_prefix, row.files.filter((f) => f.path === a.path));
489
+ const [file] = await readCatalogFiles(this.files, v.object_prefix, v.files.filter((f) => f.path === a.path));
499
490
  if (!file)
500
491
  throw new ApiError("internal", `Stored agent template ${a.path} is missing`);
501
492
  const id = live?.id ?? randomUUID();
502
493
  const to = (live?.current_version ?? 0) + 1;
494
+ const name = file.path.split("/").pop();
503
495
  plan.agentTemplates.push({
504
496
  name: a.template.name,
505
497
  base: live && { id: live.id, version: live.current_version },
506
498
  id,
507
499
  to,
508
- prefix: syncVersionPrefix(platformAgentTemplatePrefix(id, to), version),
509
- file: { ...file, path: file.path.split("/").pop() },
500
+ prefix: contentPrefix("agent-templates", [{ path: name, sha256: file.sha256 }]),
501
+ file: { ...file, path: name },
510
502
  template: a.template,
511
503
  });
512
504
  }
@@ -554,9 +546,7 @@ let PlatformComponents = class PlatformComponents {
554
546
  base: live && { id: live.id, version: live.current_version },
555
547
  id,
556
548
  to,
557
- prefix: t.assets.length > 0
558
- ? syncVersionPrefix(`${platformThemePrefix(id)}${to}/`, version)
559
- : null,
549
+ prefix: t.assets.length > 0 ? contentPrefix("themes", t.assets) : null,
560
550
  files: t.assets,
561
551
  theme: t.theme,
562
552
  });
@@ -1,5 +1,4 @@
1
1
  import { type AgentTemplate, type AgentTemplateTier, type LocalizedName, type PluginManifest, type ThemeScheme } from "@capybara-hq/plugin-sdk";
2
- import type { Queue } from "bullmq";
3
2
  import { ClientBuilds } from "../client/client-builds.service.js";
4
3
  import { ApiError } from "../errors.js";
5
4
  import { ExtensionAdminOps } from "../extensions/admin-ops.js";
@@ -16,6 +15,7 @@ import { SuperAdminLock } from "../identity/super-admin.js";
16
15
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
17
16
  import { KernelDb } from "../infra/database/kernel-db.js";
18
17
  import { TransactionHost } from "../infra/database/transaction-host.js";
18
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
19
19
  import type { KernelLogger } from "../log.js";
20
20
  import { OrgLock } from "../orgs/org-lock.js";
21
21
  import type { ProvenanceResult } from "../packages/provenance.js";
@@ -27,8 +27,6 @@ import { CatalogItemLock } from "./locks.js";
27
27
  import { PlatformComponents } from "./platform-components.js";
28
28
  import { PluginPackageError } from "./plugin-package.js";
29
29
  import type { PluginScope } from "./scope.js";
30
- /** How long after a failed post-import platform sync the migration job runs for the item. */
31
- export declare const PLATFORM_SYNC_RETRY_MS = 60000;
32
30
  /**
33
31
  * Platform import of a plugin (package ecosystem spec §4.3): one review for every plugin, whatever
34
32
  * it contains (an extension, skills, connector templates, dependencies), confirmed once.
@@ -54,8 +52,10 @@ export declare const PLATFORM_SYNC_RETRY_MS = 60000;
54
52
  * snapshot when it is installed or upgraded by this import (source web, audit
55
53
  * `extension.permissions_approved` carrying the plugin), and creates the item (with the scope the
56
54
  * admin chose, default `optional`) or adds the version (`manifest` = the normalized plugin.json,
57
- * `extension_sha256`, the files under the staging prefix). After the commit the plugin's own
58
- * extension is installed on the deployment and enabled (`ExtensionAdminOps.install` + `enable`;
55
+ * `extension_sha256`, the files under the staging prefix); an item's first usable version also
56
+ * gets its platform component rows there (their files copied before, keyed by content: a failure
57
+ * leaves nothing a retry copies again; no job heals an item later). After the commit the plugin's
58
+ * own extension is installed on the deployment and enabled (`ExtensionAdminOps.install` + `enable`;
59
59
  * each replica reconciles, no restart); a failure is returned, not thrown, and an upgrade that was
60
60
  * not installed gets its old snapshot back.
61
61
  *
@@ -413,12 +413,14 @@ export declare class PluginImport {
413
413
  private readonly registry;
414
414
  private readonly log;
415
415
  readonly txHost: TransactionHost;
416
- /** The platform rows of a new item's components, written after the confirm's commit. */
416
+ /** The platform rows of a new item's components, written in the confirm's transaction. */
417
417
  private readonly platform;
418
- /** A failed platform sync enqueues a run of the migration job here (it heals the item). */
419
- private readonly maintenance;
420
418
  /** The client build of a dropped org upload's package is deleted with it. */
421
419
  private readonly clientBuilds;
420
+ /** The scope sync's backstop, owed by the confirm's commit (ruling R24). */
421
+ private readonly outbox;
422
+ /** The backstop's delay (a test seam; default `SCOPE_BACKSTOP_DELAY_MS`). */
423
+ private readonly backstopDelayMs;
422
424
  /** Where plugin files go (the kernel's object storage; the CLI's own, see {@link useCliStorage}). */
423
425
  private files;
424
426
  /**
@@ -436,12 +438,14 @@ export declare class PluginImport {
436
438
  blobs: ExtensionBlobs,
437
439
  /** Config-declared names, loaded connectors and extension versions (the extension registry). */
438
440
  registry: ExtensionRegistry, log: KernelLogger, txHost: TransactionHost,
439
- /** The platform rows of a new item's components, written after the confirm's commit. */
441
+ /** The platform rows of a new item's components, written in the confirm's transaction. */
440
442
  platform: PlatformComponents,
441
- /** A failed platform sync enqueues a run of the migration job here (it heals the item). */
442
- maintenance: Pick<Queue, "add">,
443
443
  /** The client build of a dropped org upload's package is deleted with it. */
444
- clientBuilds: ClientBuilds);
444
+ clientBuilds: ClientBuilds,
445
+ /** The scope sync's backstop, owed by the confirm's commit (ruling R24). */
446
+ outbox: Pick<OutboxService, "add">,
447
+ /** The backstop's delay (a test seam; default `SCOPE_BACKSTOP_DELAY_MS`). */
448
+ backstopDelayMs?: number);
445
449
  /** The kernel-schema handle of the current context (the transaction, else the pool). */
446
450
  private get db();
447
451
  /**
@@ -587,11 +591,12 @@ export declare class PluginImport {
587
591
  owner?: ImportOwner;
588
592
  }): Promise<PluginConfirmResult>;
589
593
  /**
590
- * Writes the platform components of an item's first usable version after a commit made
591
- * elsewhere (the super admin's confirmation of an org upload, `CatalogScopeService`); a failure
592
- * is logged and retried by the migration job (see {@link syncPlatform}).
594
+ * The platform components of the staged version when its confirm will most likely make it the
595
+ * item's first usable version ({@link commit} decides under its locks): no live item of the name
596
+ * yet, or an org upload replacing its own first upload still awaiting confirmation. Planned from
597
+ * the staged content and their files copied, outside any transaction; otherwise undefined.
593
598
  */
594
- writePlatformComponents(itemId: string, version: string): Promise<void>;
599
+ private prepareFirstVersion;
595
600
  /**
596
601
  * The confirm's transaction: the staging row consumed, the extension's action recomputed under
597
602
  * its install lock (stale -> `plugin.review_stale`), its snapshot approved, and the catalog
@@ -605,17 +610,15 @@ export declare class PluginImport {
605
610
  * upload without an extension for an item whose first version still waits (plan Task 6) replaces
606
611
  * that upload and applies at once: the item stops waiting and the claim is released (nothing
607
612
  * was installed under it).
613
+ *
614
+ * The item's first usable version (a new item not waiting, or an awaiting first upload replaced)
615
+ * writes the platform rows of its skills, agent templates, connector templates and themes
616
+ * (plugin scope phase 2) from `prepared` (their files copied before the transaction, keyed by
617
+ * content): a failure rolls the whole confirm back, and a retry copies nothing again. A plan made
618
+ * for another item or version, or a component changed since it -> `plugin.review_stale`. A new
619
+ * version of an existing item leaves them to the platform update.
608
620
  */
609
621
  private commit;
610
- /**
611
- * The platform components of a new item. The item is committed already, so a failure is logged
612
- * and not thrown (like a failed deploy, the import itself succeeded); a run of the platform
613
- * components migration job (which writes what is missing) is enqueued on `cap-kernel-maintenance`
614
- * after {@link PLATFORM_SYNC_RETRY_MS}, one per item (deterministic job id). If that run fails
615
- * too, its `itemsFailed` shows in the super admin's inbox; the daily run and every worker start
616
- * try again.
617
- */
618
- private syncPlatform;
619
622
  /**
620
623
  * Writes the extension's permission snapshot for the version being imported (source web; cli
621
624
  * without an actor; bootstrap for the bootstrap step), after