@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 AvailableHook, type HookHandler, type HookOptions, type HookPayloads, type Logger, type PolicyHook } from "@capybara-hq/plugin-sdk";
1
+ import { type AvailableHook, type HookHandler, type HookOptions, type HookPayloads, type LIFECYCLE_HOOKS, type Logger, type PolicyHook } from "@capybara-hq/plugin-sdk";
2
2
  import type { KernelConfig } from "../config/schema.js";
3
3
  import type { LockService } from "../infra/lock/lock.service.js";
4
4
  import { type EffectiveConfig } from "./effective-config.js";
@@ -63,7 +63,16 @@ export interface HookSource {
63
63
  readonly name: string;
64
64
  readonly hooks: readonly RegisteredHook[];
65
65
  readonly inflight: InflightTracker;
66
+ /** The instance's platform deployment config (what lifecycle hooks are called with). */
67
+ readonly config?: unknown;
66
68
  }
69
+ /** A lifecycle mount: an observe-only notice that a member, an agent or an org was removed. */
70
+ export type LifecycleHook = (typeof LIFECYCLE_HOOKS)[number];
71
+ /**
72
+ * Mounts delivered by a queue (at least once, by plugin) instead of {@link HookRegistry.run}:
73
+ * `usage.report` and the lifecycle mounts.
74
+ */
75
+ export type QueuedHook = "usage.report" | LifecycleHook;
67
76
  /** Error text that never throws itself (a hostile `message` getter or `toString`). */
68
77
  export declare function errMsg(e: unknown): string;
69
78
  /**
@@ -166,8 +175,18 @@ export declare class HookRegistry {
166
175
  plugin: string;
167
176
  handler: HookHandler<"usage.report">;
168
177
  }[];
169
- /** Runs the hooks of a mount point (see the class comment). */
170
- run<K extends Exclude<AvailableHook, "usage.report">>(mount: K, payload: HookPayloads[K], signal: AbortSignal): Promise<HookOutcome>;
178
+ /**
179
+ * Lifecycle handlers by plugin name (delivered by the lifecycle queue, `lifecycle-hooks.ts`, not
180
+ * by {@link run}). Each call runs through the instance's `inflight.run` with the plugin's platform
181
+ * deployment config, never an org's copy: cleanup goes out whether or not the org's effective
182
+ * config is valid (or the org still exists).
183
+ */
184
+ lifecycleHandlers<K extends LifecycleHook>(mount: K): {
185
+ plugin: string;
186
+ handler: HookHandler<K>;
187
+ }[];
188
+ /** Runs the hooks of a mount point (see the class comment); queue-delivered mounts excluded. */
189
+ run<K extends Exclude<AvailableHook, QueuedHook>>(mount: K, payload: HookPayloads[K], signal: AbortSignal): Promise<HookOutcome>;
171
190
  /**
172
191
  * The M-1 denial: `missing` (sorted) are enabled policy plugins that are not loaded on this replica.
173
192
  * The model-visible reason is generic; the detail and a throttled warning name the plugin for the
@@ -189,7 +189,21 @@ export class HookRegistry {
189
189
  }),
190
190
  }));
191
191
  }
192
- /** Runs the hooks of a mount point (see the class comment). */
192
+ /**
193
+ * Lifecycle handlers by plugin name (delivered by the lifecycle queue, `lifecycle-hooks.ts`, not
194
+ * by {@link run}). Each call runs through the instance's `inflight.run` with the plugin's platform
195
+ * deployment config, never an org's copy: cleanup goes out whether or not the org's effective
196
+ * config is valid (or the org still exists).
197
+ */
198
+ lifecycleHandlers(mount) {
199
+ return this.entries(mount).map(({ src, hook }) => ({
200
+ plugin: src.name,
201
+ handler: (payload, signal) => src.inflight.run(signal, async (s) => {
202
+ await hook.handler(payload, s, { config: src.config });
203
+ }),
204
+ }));
205
+ }
206
+ /** Runs the hooks of a mount point (see the class comment); queue-delivered mounts excluded. */
193
207
  async run(mount, payload, signal) {
194
208
  if (signal.aborted)
195
209
  throw signal.reason;
@@ -0,0 +1,99 @@
1
+ import type { HookPayloads } from "@capybara-hq/plugin-sdk";
2
+ import { WorkerHost } from "@nestjs/bullmq";
3
+ import type { Job, Worker } from "bullmq";
4
+ import type { KernelSchemaDb } from "../db/kernel-schema.js";
5
+ import { KernelDb } from "../infra/database/kernel-db.js";
6
+ import { TransactionHost } from "../infra/database/transaction-host.js";
7
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
8
+ import type { KernelLogger } from "../log.js";
9
+ import { type LifecycleHook } from "./hooks.js";
10
+ import { ExtensionHost } from "./host.js";
11
+ /** Queue of lifecycle hook deliveries (no `:` in the name). */
12
+ export declare const LIFECYCLE_QUEUE = "cap-kernel-lifecycle";
13
+ /** The job name of every delivery (registered in `infra/outbox/registry.ts`). */
14
+ export declare const LIFECYCLE_JOB = "lifecycle";
15
+ /** One handler call's time limit. */
16
+ export declare const LIFECYCLE_HOOK_TIMEOUT_MS = 5000;
17
+ /** Job data: one mount's payload for one subscribing plugin. */
18
+ export interface LifecycleJob<K extends LifecycleHook = LifecycleHook> {
19
+ mount: K;
20
+ plugin: string;
21
+ payload: HookPayloads[K];
22
+ }
23
+ /**
24
+ * The deterministic job id of one delivery: the mount, the plugin and the payload's ids (no `:`;
25
+ * kernel ids and plugin names carry none). Removing the same member, agent or org again while the
26
+ * first delivery is still owed is the same job.
27
+ */
28
+ export declare function lifecycleJobId<K extends LifecycleHook>(mount: K, plugin: string, payload: HookPayloads[K]): string;
29
+ /**
30
+ * Plugins that subscribe to a lifecycle mount according to deployment state: the approved
31
+ * permission snapshot declares the mount in `permissions.hooks`, and that approved version is the
32
+ * one the deployment runs: an enabled install of that version, or a replica status row (not
33
+ * `unloaded`) that loaded or still serves it (an upgrade pending approval keeps serving the
34
+ * approved version; a config-declared plugin has only replica rows). A snapshot approved for
35
+ * another version than the one installed or served does not count. Org enablement does not count
36
+ * either: cleanup concerns data the plugin already holds (memory plugin spec §6.3). Sorted by name.
37
+ */
38
+ export declare function queryLifecycleSubscribers(db: KernelSchemaDb, mount: LifecycleHook): Promise<string[]>;
39
+ /**
40
+ * Durable delivery of the lifecycle hooks `org.member_removed`, `agent.deleted` and `org.deleted`
41
+ * (memory plugin spec §6.3), so extensions reliably learn to delete the data they hold:
42
+ * - **Owe** ({@link recordIn}): in the transaction that removes the member, the agent or the org,
43
+ * one outbox job per subscribing plugin ({@link queryLifecycleSubscribers}) on
44
+ * {@link LIFECYCLE_QUEUE}, under {@link lifecycleJobId}, with no `org_id` (it is the deletion's
45
+ * own cleanup work, transactional outbox spec §9); a rollback owes nothing. The outbox delivers
46
+ * it to BullMQ after the commit (registry: attempts and backoff).
47
+ * - **Deliver** ({@link process}, worker role): the plugin's handlers loaded on this replica are
48
+ * called in turn (5 s each). Not loaded on this replica -> the job fails and is retried (another
49
+ * replica or a later attempt). Loaded without a handler -> dropped with a warning. Handlers get
50
+ * the plugin's platform deployment config (`call.config`), never an org's copy, so an org whose
51
+ * effective config is not valid still gets its cleanup. The last failed attempt logs an error.
52
+ * Handlers must be idempotent (a delivery may repeat when a replica dies after the call).
53
+ */
54
+ export declare class LifecycleHooks {
55
+ private readonly kdb;
56
+ readonly txHost: TransactionHost;
57
+ private readonly outbox;
58
+ /** The loaded plugins' handlers (`hooks.handlers`) and whether a plugin is active here. */
59
+ private readonly host;
60
+ private readonly log;
61
+ constructor(kdb: KernelDb, txHost: TransactionHost, outbox: Pick<OutboxService, "add">,
62
+ /** The loaded plugins' handlers (`hooks.handlers`) and whether a plugin is active here. */
63
+ host: Pick<ExtensionHost, "hooks" | "current">, log: KernelLogger);
64
+ /**
65
+ * Owes one delivery per subscribing plugin in the caller's transaction (`mandatory`); returns
66
+ * the plugins.
67
+ */
68
+ recordIn<K extends LifecycleHook>(mount: K, payload: HookPayloads[K]): Promise<string[]>;
69
+ /** One delivery job (the consumer {@link LifecycleHooksProcessor} calls it). */
70
+ process(data: LifecycleJob, stop: AbortSignal): Promise<void>;
71
+ /** One handler call, aborted after {@link LIFECYCLE_HOOK_TIMEOUT_MS} or when the replica stops. */
72
+ private call;
73
+ /** A job failed (the worker's `failed` event): a retry warns, the last attempt logs an error. */
74
+ onFailed(j: Job<LifecycleJob> | undefined, err: Error): void;
75
+ }
76
+ /**
77
+ * The consumer of {@link LIFECYCLE_QUEUE}: {@link LifecycleHooks.process}. Worker role only
78
+ * (`LifecycleHookWorkersModule`); built with `autorun: false`, started by the worker role after the
79
+ * extensions loaded.
80
+ */
81
+ export declare class LifecycleHooksProcessor extends WorkerHost<Worker<LifecycleJob>> {
82
+ private readonly hooks;
83
+ private readonly log;
84
+ private readonly stopping;
85
+ constructor(hooks: LifecycleHooks, log: Pick<KernelLogger, "error">);
86
+ process(job: Job<LifecycleJob>): Promise<void>;
87
+ /** Starts consuming (worker role, after the extensions loaded). */
88
+ start(): void;
89
+ /** Aborts the calls in flight and closes the worker. */
90
+ close(): Promise<void>;
91
+ failed(job: Job<LifecycleJob> | undefined, err: Error): void;
92
+ error(err: Error): void;
93
+ }
94
+ /** Lifecycle hook delivery: owing the jobs, and the queue the outbox adds them to, on every role. */
95
+ export declare class LifecycleHooksModule {
96
+ }
97
+ /** The lifecycle queue's consumer; imported only by the worker role (`WorkerModule`). */
98
+ export declare class LifecycleHookWorkersModule {
99
+ }
@@ -0,0 +1,273 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { OnWorkerEvent, Processor, WorkerHost } from "@nestjs/bullmq";
11
+ import { Inject, Injectable, Module } from "@nestjs/common";
12
+ import { sql } from "kysely";
13
+ import { KernelDb } from "../infra/database/kernel-db.js";
14
+ import { TransactionHost } from "../infra/database/transaction-host.js";
15
+ import { Transactional } from "../infra/database/transactional.decorator.js";
16
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
17
+ import { QueueModule } from "../infra/queue/queue.module.js";
18
+ import { runJob } from "../infra/queue/run-job.js";
19
+ import { KERNEL_LOGGER } from "../infra/tokens.js";
20
+ import { ExtensionsModule } from "./extensions.module.js";
21
+ import { deepFreeze, errMsg } from "./hooks.js";
22
+ import { ExtensionHost } from "./host.js";
23
+ /** Queue of lifecycle hook deliveries (no `:` in the name). */
24
+ export const LIFECYCLE_QUEUE = "cap-kernel-lifecycle";
25
+ /** The job name of every delivery (registered in `infra/outbox/registry.ts`). */
26
+ export const LIFECYCLE_JOB = "lifecycle";
27
+ /** One handler call's time limit. */
28
+ export const LIFECYCLE_HOOK_TIMEOUT_MS = 5000;
29
+ /** Concurrent deliveries per worker replica. */
30
+ const LIFECYCLE_CONCURRENCY = 4;
31
+ /**
32
+ * The deterministic job id of one delivery: the mount, the plugin and the payload's ids (no `:`;
33
+ * kernel ids and plugin names carry none). Removing the same member, agent or org again while the
34
+ * first delivery is still owed is the same job.
35
+ */
36
+ export function lifecycleJobId(mount, plugin, payload) {
37
+ const ids = Object.entries(payload)
38
+ .sort(([a], [b]) => (a < b ? -1 : 1))
39
+ .map(([, v]) => String(v));
40
+ return ["lifecycle", mount.replaceAll(".", "-"), plugin, ...ids].join("-").replaceAll(":", "-");
41
+ }
42
+ /**
43
+ * Plugins that subscribe to a lifecycle mount according to deployment state: the approved
44
+ * permission snapshot declares the mount in `permissions.hooks`, and that approved version is the
45
+ * one the deployment runs: an enabled install of that version, or a replica status row (not
46
+ * `unloaded`) that loaded or still serves it (an upgrade pending approval keeps serving the
47
+ * approved version; a config-declared plugin has only replica rows). A snapshot approved for
48
+ * another version than the one installed or served does not count. Org enablement does not count
49
+ * either: cleanup concerns data the plugin already holds (memory plugin spec §6.3). Sorted by name.
50
+ */
51
+ export async function queryLifecycleSubscribers(db, mount) {
52
+ const rows = await db
53
+ .selectFrom("extension_permission_snapshots as s")
54
+ .select("s.plugin")
55
+ .distinct()
56
+ .where(sql `s.permissions -> 'permissions' -> 'hooks' ? ${mount}`)
57
+ .where((eb) => eb.or([
58
+ eb.exists(eb
59
+ .selectFrom("extension_installs as i")
60
+ .select(sql `1`.as("one"))
61
+ .whereRef("i.name", "=", "s.plugin")
62
+ .whereRef("i.version", "=", "s.version")
63
+ .where("i.desired_state", "=", "enabled")),
64
+ eb.exists(eb
65
+ .selectFrom("extension_replica_status as r")
66
+ .select(sql `1`.as("one"))
67
+ .whereRef("r.plugin", "=", "s.plugin")
68
+ .where("r.state", "<>", "unloaded")
69
+ .where((r) => r.or([
70
+ r("r.version", "=", r.ref("s.version")),
71
+ r("r.serving_version", "=", r.ref("s.version")),
72
+ ]))),
73
+ ]))
74
+ .orderBy("s.plugin")
75
+ .execute();
76
+ return rows.map((r) => r.plugin);
77
+ }
78
+ /**
79
+ * Durable delivery of the lifecycle hooks `org.member_removed`, `agent.deleted` and `org.deleted`
80
+ * (memory plugin spec §6.3), so extensions reliably learn to delete the data they hold:
81
+ * - **Owe** ({@link recordIn}): in the transaction that removes the member, the agent or the org,
82
+ * one outbox job per subscribing plugin ({@link queryLifecycleSubscribers}) on
83
+ * {@link LIFECYCLE_QUEUE}, under {@link lifecycleJobId}, with no `org_id` (it is the deletion's
84
+ * own cleanup work, transactional outbox spec §9); a rollback owes nothing. The outbox delivers
85
+ * it to BullMQ after the commit (registry: attempts and backoff).
86
+ * - **Deliver** ({@link process}, worker role): the plugin's handlers loaded on this replica are
87
+ * called in turn (5 s each). Not loaded on this replica -> the job fails and is retried (another
88
+ * replica or a later attempt). Loaded without a handler -> dropped with a warning. Handlers get
89
+ * the plugin's platform deployment config (`call.config`), never an org's copy, so an org whose
90
+ * effective config is not valid still gets its cleanup. The last failed attempt logs an error.
91
+ * Handlers must be idempotent (a delivery may repeat when a replica dies after the call).
92
+ */
93
+ let LifecycleHooks = class LifecycleHooks {
94
+ kdb;
95
+ txHost;
96
+ outbox;
97
+ host;
98
+ log;
99
+ constructor(kdb, txHost, outbox,
100
+ /** The loaded plugins' handlers (`hooks.handlers`) and whether a plugin is active here. */
101
+ host, log) {
102
+ this.kdb = kdb;
103
+ this.txHost = txHost;
104
+ this.outbox = outbox;
105
+ this.host = host;
106
+ this.log = log;
107
+ }
108
+ /**
109
+ * Owes one delivery per subscribing plugin in the caller's transaction (`mandatory`); returns
110
+ * the plugins.
111
+ */
112
+ async recordIn(mount, payload) {
113
+ const plugins = await queryLifecycleSubscribers(this.kdb.db, mount);
114
+ for (const plugin of plugins) {
115
+ const data = { mount, plugin, payload };
116
+ await this.outbox.add({
117
+ queue: LIFECYCLE_QUEUE,
118
+ name: LIFECYCLE_JOB,
119
+ jobId: lifecycleJobId(mount, plugin, payload),
120
+ data,
121
+ });
122
+ }
123
+ return plugins;
124
+ }
125
+ /** One delivery job (the consumer {@link LifecycleHooksProcessor} calls it). */
126
+ async process(data, stop) {
127
+ const fields = { mount: data.mount, plugin: data.plugin };
128
+ const handlers = this.host.hooks
129
+ .lifecycleHandlers(data.mount)
130
+ .filter((h) => h.plugin === data.plugin)
131
+ .map((h) => h.handler);
132
+ if (handlers.length === 0) {
133
+ if (this.host.current(data.plugin) === undefined) {
134
+ // another replica or a later attempt may have it loaded
135
+ throw new Error(`plugin ${data.plugin} is not loaded on this replica`);
136
+ }
137
+ this.log.warn("lifecycle hook delivery dropped: the plugin registered no handler", fields);
138
+ return;
139
+ }
140
+ const payload = deepFreeze(structuredClone(data.payload));
141
+ for (const h of handlers)
142
+ await this.call(h, payload, stop);
143
+ }
144
+ /** One handler call, aborted after {@link LIFECYCLE_HOOK_TIMEOUT_MS} or when the replica stops. */
145
+ async call(h, payload, stop) {
146
+ const ac = new AbortController();
147
+ const onStop = () => ac.abort(new Error("replica stopping"));
148
+ if (stop.aborted)
149
+ onStop();
150
+ else
151
+ stop.addEventListener("abort", onStop, { once: true });
152
+ let timer;
153
+ try {
154
+ await Promise.race([
155
+ Promise.resolve().then(() => h(payload, ac.signal)),
156
+ new Promise((_, reject) => {
157
+ timer = setTimeout(() => {
158
+ const err = new Error(`timed out after ${LIFECYCLE_HOOK_TIMEOUT_MS} ms`);
159
+ ac.abort(err);
160
+ reject(err);
161
+ }, LIFECYCLE_HOOK_TIMEOUT_MS);
162
+ // already aborted (the replica was stopping before the call): no abort event will fire
163
+ if (ac.signal.aborted)
164
+ reject(ac.signal.reason);
165
+ else
166
+ ac.signal.addEventListener("abort", () => reject(ac.signal.reason), { once: true });
167
+ }),
168
+ ]);
169
+ }
170
+ finally {
171
+ clearTimeout(timer);
172
+ stop.removeEventListener("abort", onStop);
173
+ }
174
+ }
175
+ /** A job failed (the worker's `failed` event): a retry warns, the last attempt logs an error. */
176
+ onFailed(j, err) {
177
+ if (!j)
178
+ return;
179
+ const fields = {
180
+ jobId: j.id,
181
+ mount: j.data.mount,
182
+ plugin: j.data.plugin,
183
+ attempt: j.attemptsMade,
184
+ error: errMsg(err),
185
+ };
186
+ if (j.attemptsMade >= (j.opts.attempts ?? 1)) {
187
+ this.log.error("lifecycle hook delivery failed on its last attempt; given up", fields);
188
+ }
189
+ else
190
+ this.log.warn("lifecycle hook delivery failed; retrying", fields);
191
+ }
192
+ };
193
+ __decorate([
194
+ Transactional({ propagation: "mandatory" })
195
+ ], LifecycleHooks.prototype, "recordIn", null);
196
+ LifecycleHooks = __decorate([
197
+ Injectable(),
198
+ __param(0, Inject(KernelDb)),
199
+ __param(1, Inject(TransactionHost)),
200
+ __param(2, Inject(OutboxService)),
201
+ __param(3, Inject(ExtensionHost)),
202
+ __param(4, Inject(KERNEL_LOGGER))
203
+ ], LifecycleHooks);
204
+ export { LifecycleHooks };
205
+ /**
206
+ * The consumer of {@link LIFECYCLE_QUEUE}: {@link LifecycleHooks.process}. Worker role only
207
+ * (`LifecycleHookWorkersModule`); built with `autorun: false`, started by the worker role after the
208
+ * extensions loaded.
209
+ */
210
+ let LifecycleHooksProcessor = class LifecycleHooksProcessor extends WorkerHost {
211
+ hooks;
212
+ log;
213
+ stopping = new AbortController();
214
+ constructor(hooks, log) {
215
+ super();
216
+ this.hooks = hooks;
217
+ this.log = log;
218
+ }
219
+ process(job) {
220
+ return runJob(LIFECYCLE_QUEUE, job, () => this.hooks.process(job.data, this.stopping.signal));
221
+ }
222
+ /** Starts consuming (worker role, after the extensions loaded). */
223
+ start() {
224
+ void this.worker.run().catch((err) => this.error(err));
225
+ }
226
+ /** Aborts the calls in flight and closes the worker. */
227
+ async close() {
228
+ this.stopping.abort();
229
+ await this.worker.close();
230
+ }
231
+ failed(job, err) {
232
+ this.hooks.onFailed(job, err);
233
+ }
234
+ error(err) {
235
+ this.log.error("lifecycle hook worker error", { err });
236
+ }
237
+ };
238
+ __decorate([
239
+ OnWorkerEvent("failed")
240
+ ], LifecycleHooksProcessor.prototype, "failed", null);
241
+ __decorate([
242
+ OnWorkerEvent("error")
243
+ ], LifecycleHooksProcessor.prototype, "error", null);
244
+ LifecycleHooksProcessor = __decorate([
245
+ Injectable(),
246
+ Processor(LIFECYCLE_QUEUE, { autorun: false, concurrency: LIFECYCLE_CONCURRENCY }),
247
+ __param(0, Inject(LifecycleHooks)),
248
+ __param(1, Inject(KERNEL_LOGGER))
249
+ ], LifecycleHooksProcessor);
250
+ export { LifecycleHooksProcessor };
251
+ const LIFECYCLE_QUEUE_MODULE = QueueModule.registerQueue(LIFECYCLE_QUEUE);
252
+ /** Lifecycle hook delivery: owing the jobs, and the queue the outbox adds them to, on every role. */
253
+ let LifecycleHooksModule = class LifecycleHooksModule {
254
+ };
255
+ LifecycleHooksModule = __decorate([
256
+ Module({
257
+ imports: [ExtensionsModule, LIFECYCLE_QUEUE_MODULE],
258
+ providers: [LifecycleHooks],
259
+ exports: [LifecycleHooks, LIFECYCLE_QUEUE_MODULE],
260
+ })
261
+ ], LifecycleHooksModule);
262
+ export { LifecycleHooksModule };
263
+ /** The lifecycle queue's consumer; imported only by the worker role (`WorkerModule`). */
264
+ let LifecycleHookWorkersModule = class LifecycleHookWorkersModule {
265
+ };
266
+ LifecycleHookWorkersModule = __decorate([
267
+ Module({
268
+ imports: [LifecycleHooksModule],
269
+ providers: [LifecycleHooksProcessor],
270
+ exports: [LifecycleHooksProcessor],
271
+ })
272
+ ], LifecycleHookWorkersModule);
273
+ export { LifecycleHookWorkersModule };
@@ -110,6 +110,16 @@ export interface OrgExtensionPutOptions {
110
110
  */
111
111
  validateOnly?: boolean;
112
112
  }
113
+ /**
114
+ * An org's stored config of a plugin no longer matches its org schema (e.g. after a plugin upgrade):
115
+ * a known state, not a read error. The message names the plugin, the org and the failing paths,
116
+ * never the values.
117
+ */
118
+ export declare class OrgConfigInvalidError extends Error {
119
+ readonly plugin: string;
120
+ readonly orgId: string;
121
+ constructor(plugin: string, orgId: string, detail: string);
122
+ }
113
123
  /**
114
124
  * Org-level extension config: enablement and the `config.org` config stored per org.
115
125
  * The non-secret part is stored in `capybara.extension_org_configs.config`; each secret field is
@@ -210,7 +220,9 @@ export declare class OrgExtensionConfigs {
210
220
  * A required bootstrap plugin is enabled without a row (its stored config, or `{}`, is validated).
211
221
  * 无记录 → undefined;插件无 `config.org` 或已停用(`enabled=false`,不校验、不读密钥)时 `config` 为 null。
212
222
  * 配置行与密钥在同一个 `REPEATABLE READ` 只读事务里读取。
213
- * 已存配置不再满足 schema(如插件升级后)→ 抛 Error,信息含插件名、组织 ID 与出错路径(不含值)。
223
+ * A stored config that no longer matches the schema (e.g. after a plugin upgrade) throws
224
+ * {@link OrgConfigInvalidError} (plugin, org ID and the failing paths, no values); other errors are
225
+ * read errors.
214
226
  */
215
227
  get(orgId: string, plugin: string): Promise<{
216
228
  enabled: boolean;
@@ -88,6 +88,21 @@ function secretPaths(schema) {
88
88
  .filter((f) => f.meta?.secret)
89
89
  .map((f) => f.path);
90
90
  }
91
+ /**
92
+ * An org's stored config of a plugin no longer matches its org schema (e.g. after a plugin upgrade):
93
+ * a known state, not a read error. The message names the plugin, the org and the failing paths,
94
+ * never the values.
95
+ */
96
+ export class OrgConfigInvalidError extends Error {
97
+ plugin;
98
+ orgId;
99
+ constructor(plugin, orgId, detail) {
100
+ super(`plugin ${plugin}: stored organization config for org ${orgId} is invalid: ${detail}`);
101
+ this.plugin = plugin;
102
+ this.orgId = orgId;
103
+ this.name = "OrgConfigInvalidError";
104
+ }
105
+ }
91
106
  /**
92
107
  * Org-level extension config: enablement and the `config.org` config stored per org.
93
108
  * The non-secret part is stored in `capybara.extension_org_configs.config`; each secret field is
@@ -444,7 +459,9 @@ let OrgExtensionConfigs = class OrgExtensionConfigs {
444
459
  * A required bootstrap plugin is enabled without a row (its stored config, or `{}`, is validated).
445
460
  * 无记录 → undefined;插件无 `config.org` 或已停用(`enabled=false`,不校验、不读密钥)时 `config` 为 null。
446
461
  * 配置行与密钥在同一个 `REPEATABLE READ` 只读事务里读取。
447
- * 已存配置不再满足 schema(如插件升级后)→ 抛 Error,信息含插件名、组织 ID 与出错路径(不含值)。
462
+ * A stored config that no longer matches the schema (e.g. after a plugin upgrade) throws
463
+ * {@link OrgConfigInvalidError} (plugin, org ID and the failing paths, no values); other errors are
464
+ * read errors.
448
465
  */
449
466
  async get(orgId, plugin) {
450
467
  const manifest = this.manifest(plugin);
@@ -488,7 +505,7 @@ let OrgExtensionConfigs = class OrgExtensionConfigs {
488
505
  const detail = toIssues(r.error)
489
506
  .map((i) => `${i.path || "<root>"}: ${i.message}`)
490
507
  .join("; ");
491
- throw new Error(`plugin ${plugin}: stored organization config for org ${orgId} is invalid: ${detail}`);
508
+ throw new OrgConfigInvalidError(plugin, orgId, detail);
492
509
  }
493
510
  return { enabled: true, config: r.data };
494
511
  }
@@ -30,7 +30,7 @@ export interface PermissionGroup {
30
30
  * ({@link SENSITIVE_EXTENSION_POINTS}; the code is the extension point name without `@<major>`, with
31
31
  * `-` replaced by `_`).
32
32
  */
33
- export type SensitiveReason = "secrets" | "public_routes" | "tools" | "hooks" | "ui_embed" | "client_code" | "ui_component" | "channel" | "directory" | "identity_source" | "sandbox" | "object_store" | "migration_warnings";
33
+ export type SensitiveReason = "secrets" | "public_routes" | "tools" | "hooks" | "ui_embed" | "client_code" | "ui_component" | "channel" | "directory" | "identity_source" | "sandbox" | "object_store" | "context" | "migration_warnings";
34
34
  /** 权限清单里的一项(分组里的一项,或 `added` 里的一个权限键)。 */
35
35
  export interface PermissionItem {
36
36
  value: string;
@@ -39,10 +39,14 @@ export interface PermissionItem {
39
39
  reason?: SensitiveReason;
40
40
  }
41
41
  /**
42
- * 敏感的能力(D10 + 计划扩展,F-AI):`sandbox@2` 在部署里执行任意命令、`object-store@1` 保管全部附件与插件包(D10);
43
- * `channel@1` 收到组织在 IM 里的对话内容(Ruling A33)、`directory@1` 能把人加进组织(并为其建账号、建身份)、
44
- * `identity-source@1` 决定谁能登录成哪个账号。插件提供其中任何一个时,「提供」一组标为敏感,
45
- * 后台插件列表的 `sensitive` 为真,须超级管理员批准。只影响展示,不改权限快照的键。
42
+ * Sensitive capabilities (D10 + planned extensions, F-AI): `sandbox@2` runs arbitrary commands in
43
+ * the deployment, `object-store@1` holds every attachment and plugin package (D10); `channel@1`
44
+ * receives the org's IM conversations (Ruling A33), `directory@1` can add people to orgs (creating
45
+ * their accounts and identities), `identity-source@1` decides who can sign in as which account;
46
+ * `context@1` adds text to the model's context in conversations of orgs that enable it (memory
47
+ * plugin spec §6.1). A plugin providing any of them has its "provides" group marked sensitive and
48
+ * `sensitive` true in the admin plugin list, for the super admin to approve. Display only: the keys
49
+ * of the permission snapshot do not change.
46
50
  */
47
51
  export declare const SENSITIVE_EXTENSION_POINTS: readonly string[];
48
52
  /**
@@ -18,10 +18,14 @@ export function reviewHash(set, warnings = []) {
18
18
  return h.digest("hex").slice(0, 16);
19
19
  }
20
20
  /**
21
- * 敏感的能力(D10 + 计划扩展,F-AI):`sandbox@2` 在部署里执行任意命令、`object-store@1` 保管全部附件与插件包(D10);
22
- * `channel@1` 收到组织在 IM 里的对话内容(Ruling A33)、`directory@1` 能把人加进组织(并为其建账号、建身份)、
23
- * `identity-source@1` 决定谁能登录成哪个账号。插件提供其中任何一个时,「提供」一组标为敏感,
24
- * 后台插件列表的 `sensitive` 为真,须超级管理员批准。只影响展示,不改权限快照的键。
21
+ * Sensitive capabilities (D10 + planned extensions, F-AI): `sandbox@2` runs arbitrary commands in
22
+ * the deployment, `object-store@1` holds every attachment and plugin package (D10); `channel@1`
23
+ * receives the org's IM conversations (Ruling A33), `directory@1` can add people to orgs (creating
24
+ * their accounts and identities), `identity-source@1` decides who can sign in as which account;
25
+ * `context@1` adds text to the model's context in conversations of orgs that enable it (memory
26
+ * plugin spec §6.1). A plugin providing any of them has its "provides" group marked sensitive and
27
+ * `sensitive` true in the admin plugin list, for the super admin to approve. Display only: the keys
28
+ * of the permission snapshot do not change.
25
29
  */
26
30
  export const SENSITIVE_EXTENSION_POINTS = [
27
31
  "channel@1",
@@ -29,6 +33,7 @@ export const SENSITIVE_EXTENSION_POINTS = [
29
33
  "identity-source@1",
30
34
  "sandbox@2",
31
35
  "object-store@1",
36
+ "context@1",
32
37
  ];
33
38
  /** The one item of the "client" group (client extensions spec §5.4). */
34
39
  const CLIENT_ITEM = "client code";
@@ -18,6 +18,16 @@ export type PointResolution<T> = {
18
18
  ok: false;
19
19
  reason: "none" | "ambiguous";
20
20
  };
21
+ /**
22
+ * One provider of a multi-provider extension point for an org ({@link ExtensionPointDefaults.providersFor}),
23
+ * with its org config and its effective deployment config for the org, both to pass back to it.
24
+ */
25
+ export interface PointProvider<T> {
26
+ extension: string;
27
+ impl: T;
28
+ orgConfig: unknown;
29
+ config: unknown;
30
+ }
21
31
  /** One `org-default` extension point of an org (the admin view). */
22
32
  export interface OrgPointDefaultView {
23
33
  point: string;
@@ -50,6 +60,19 @@ export declare class ExtensionPointDefaults {
50
60
  * point has no `selection`.
51
61
  */
52
62
  resolve<T>(orgId: string, point: string): Promise<PointResolution<T>>;
63
+ /**
64
+ * Every provider of `point` (the name without the major, e.g. `context`) that could serve
65
+ * `orgId` now, in extension-name order: the same candidates {@link resolve} chooses from (loaded
66
+ * here, enabled in the org, valid org and effective config, `configured()` not false), for
67
+ * points whose every enabled provider is used (no `selection`). `failed`: the providers enabled
68
+ * in the org whose status could not be read (reading a config or `configured()` threw, e.g. a
69
+ * transient database error); they are neither serving nor known to be off. An invalid stored
70
+ * org config ({@link OrgConfigInvalidError}) is not a read error: that provider is off.
71
+ */
72
+ providersFor<T>(orgId: string, point: string): Promise<{
73
+ providers: PointProvider<T>[];
74
+ failed: Set<string>;
75
+ }>;
53
76
  /** Every `org-default` point with its candidates, stored default and effective provider. */
54
77
  list(orgId: string): Promise<OrgPointDefaultView[]>;
55
78
  /** Whether each `org-default` point has a provider for the org now (no extension names). */
@@ -65,7 +88,11 @@ export declare class ExtensionPointDefaults {
65
88
  setDefault(orgId: string, point: string, extension: string, actorId: string,
66
89
  /** Merged into the audit detail (a write made through the Platform tool). */
67
90
  auditDetail?: Record<string, unknown>): Promise<void>;
68
- /** The candidates of `point` in `orgId`, by extension name. */
91
+ /**
92
+ * The candidates of `point` in `orgId`, by extension name, and the enabled providers whose status
93
+ * could not be read (a read or `configured()` threw: excluded, logged). A provider whose stored
94
+ * org config or effective config is invalid is excluded but not `failed`: it is known to be off.
95
+ */
69
96
  private candidates;
70
97
  private choose;
71
98
  /** The org's stored defaults, point → extension. */