@axiom-lattice/gateway 4.8.4 → 4.9.0

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 (31) hide show
  1. package/dist/a2a-BHBLACJV.mjs +17 -0
  2. package/dist/a2a-BHBLACJV.mjs.map +1 -0
  3. package/dist/{a2a-FGYX2HH3.mjs → chunk-HYYLYIMH.mjs} +59 -15
  4. package/dist/chunk-HYYLYIMH.mjs.map +1 -0
  5. package/dist/embed/{embed-page-AgentDetailSideApp-S5N76UPU.js → embed-page-AgentDetailSideApp-BVJK7WIS.js} +1 -1
  6. package/dist/embed/{embed-page-AgentGovernanceWorkbench-QFMP2M73.js → embed-page-AgentGovernanceWorkbench-L2YHPRNE.js} +1 -1
  7. package/dist/embed/{embed-page-MessageGraph-H3HPZKOH.js → embed-page-MessageGraph-JOQ46UE2.js} +1 -1
  8. package/dist/embed/{embed-page-ReactChart-ZFMANJCT.js → embed-page-ReactChart-I5AYTSII.js} +1 -1
  9. package/dist/embed/{embed-page-SkillFlow-HY7PD43P.js → embed-page-SkillFlow-YLB5XPFH.js} +1 -1
  10. package/dist/embed/{embed-page-TasksSideApp-QKEXMHCT.js → embed-page-TasksSideApp-QLJVB7UK.js} +1 -1
  11. package/dist/embed/{embed-page-TeamWorkspace-ZFVAFZXZ.js → embed-page-TeamWorkspace-BZZEAYSB.js} +1 -1
  12. package/dist/embed/{embed-page-TopologyRuntimeView-2BS4OYEZ.js → embed-page-TopologyRuntimeView-FRA3AXQQ.js} +1 -1
  13. package/dist/embed/{embed-page-WorkflowAutomationView-WUI7BSQP.js → embed-page-WorkflowAutomationView-D745PK3W.js} +1 -1
  14. package/dist/embed/{embed-page-chunk-5WYDCL3B.js → embed-page-chunk-4WBAN7QL.js} +7 -7
  15. package/dist/embed/{embed-page-chunk-4T4QMOUS.js → embed-page-chunk-FVMSMU4J.js} +1 -1
  16. package/dist/embed/{embed-page-chunk-VJTO6SG6.js → embed-page-chunk-FWGNJMXD.js} +1 -1
  17. package/dist/embed/{embed-page-chunk-MSHEVUK3.js → embed-page-chunk-K5YYUAPL.js} +1 -1
  18. package/dist/embed/{embed-page-chunk-6HM7ZERK.js → embed-page-chunk-M2PV36MX.js} +1 -1
  19. package/dist/embed/{embed-page-chunk-XKQ6YPUW.js → embed-page-chunk-P5PFV3N7.js} +1 -1
  20. package/dist/embed/{embed-page-chunk-7ZBZKXBU.js → embed-page-chunk-WMLR2GQK.js} +4 -4
  21. package/dist/embed/embed-page.js +1 -1
  22. package/dist/index.d.mts +383 -2
  23. package/dist/index.d.ts +383 -2
  24. package/dist/index.js +2557 -1091
  25. package/dist/index.js.map +1 -1
  26. package/dist/index.mjs +1498 -79
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/{open-exposure-VU227IO3.mjs → open-exposure-27ESFWTB.mjs} +4 -4
  29. package/package.json +7 -7
  30. package/dist/a2a-FGYX2HH3.mjs.map +0 -1
  31. /package/dist/{open-exposure-VU227IO3.mjs.map → open-exposure-27ESFWTB.mjs.map} +0 -0
package/dist/index.mjs CHANGED
@@ -3,6 +3,16 @@ import {
3
3
  getFilenameFromPath,
4
4
  sendBufferWithRange
5
5
  } from "./chunk-EMYRCJOI.mjs";
6
+ import {
7
+ PathOutsideRootError,
8
+ ProjectFileValidationError,
9
+ filesystemRootDirectory,
10
+ formatFileRefsSection,
11
+ isBinaryContentType,
12
+ isPathWithinRoot,
13
+ resolvePathWithinRoot,
14
+ saveProjectFile
15
+ } from "./chunk-YKTXW6F6.mjs";
6
16
  import {
7
17
  PluginToolScopeError,
8
18
  invokePluginTool
@@ -14,6 +24,16 @@ import "./chunk-EXPERMZR.mjs";
14
24
  import {
15
25
  CollectionService
16
26
  } from "./chunk-PWHUWFXL.mjs";
27
+ import {
28
+ A2A_KEY_CHANGED_TOPIC,
29
+ handleA2AKeyChanged,
30
+ publishA2AKeyChanged,
31
+ refreshStoreKeyMap
32
+ } from "./chunk-HYYLYIMH.mjs";
33
+ import {
34
+ A2AAuthService,
35
+ parseEnvKeys
36
+ } from "./chunk-GPHOYR3X.mjs";
17
37
  import {
18
38
  createOrphanRecoveryRuntime,
19
39
  createTaskRunLiveness,
@@ -33,16 +53,6 @@ import {
33
53
  getQrCode,
34
54
  getQrCodeStatus
35
55
  } from "./chunk-6CUQGDJI.mjs";
36
- import {
37
- PathOutsideRootError,
38
- ProjectFileValidationError,
39
- filesystemRootDirectory,
40
- formatFileRefsSection,
41
- isBinaryContentType,
42
- isPathWithinRoot,
43
- resolvePathWithinRoot,
44
- saveProjectFile
45
- } from "./chunk-YKTXW6F6.mjs";
46
56
  import "./chunk-NFEGQTCC.mjs";
47
57
 
48
58
  // src/index.ts
@@ -5122,9 +5132,9 @@ var SandboxService = class {
5122
5132
  );
5123
5133
  return rewritten;
5124
5134
  }
5125
- generateErrorHtml(assistantId, threadId, vmIsolation, errorMessage4) {
5126
- const encodedError = encodeURIComponent(errorMessage4);
5127
- return ERROR_HTML.replace("{assistantId}", assistantId).replace("{threadId}", threadId).replace("{vmIsolation}", vmIsolation).replace("{errorMessage}", errorMessage4);
5135
+ generateErrorHtml(assistantId, threadId, vmIsolation, errorMessage6) {
5136
+ const encodedError = encodeURIComponent(errorMessage6);
5137
+ return ERROR_HTML.replace("{assistantId}", assistantId).replace("{threadId}", threadId).replace("{vmIsolation}", vmIsolation).replace("{errorMessage}", errorMessage6);
5128
5138
  }
5129
5139
  };
5130
5140
  var sandboxService = new SandboxService();
@@ -8642,6 +8652,16 @@ async function fetchPluginUiResource(backend, tenantId, pluginType, resource) {
8642
8652
  if (!plugin) throw new Error(`plugin "${pluginType}" not found`);
8643
8653
  const tool = (plugin.meta.tools ?? []).find((t2) => t2.ui?.resource === resource);
8644
8654
  if (!tool?.ui) throw new Error(`plugin "${pluginType}" has no tool declaring ui resource "${resource}"`);
8655
+ const inline = plugin.meta.uiResources?.[resource];
8656
+ if (inline) {
8657
+ return {
8658
+ mimeType: inline.mimeType ?? "text/html;profile=mcp-app",
8659
+ html: inline.html,
8660
+ csp: inline.csp ?? tool.ui.csp,
8661
+ permissions: tool.ui.permissions,
8662
+ prefersBorder: tool.ui.prefersBorder
8663
+ };
8664
+ }
8645
8665
  const html = await backend.catalog.readPluginFile(tenantId, pluginType, resource);
8646
8666
  return {
8647
8667
  mimeType: "text/html",
@@ -10853,9 +10873,9 @@ async function createChannelInstallation(request, reply) {
10853
10873
  };
10854
10874
  } catch (error) {
10855
10875
  console.error("Failed to create channel installation:", error);
10856
- const errorMessage4 = error instanceof Error ? error.message.toLowerCase() : "";
10876
+ const errorMessage6 = error instanceof Error ? error.message.toLowerCase() : "";
10857
10877
  const errorCode = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
10858
- if (errorMessage4.includes("duplicate") || errorMessage4.includes("unique constraint") || errorCode === "23505") {
10878
+ if (errorMessage6.includes("duplicate") || errorMessage6.includes("unique constraint") || errorCode === "23505") {
10859
10879
  reply.code(409);
10860
10880
  return {
10861
10881
  success: false,
@@ -11899,6 +11919,7 @@ import {
11899
11919
  createTaskLifecycleService,
11900
11920
  createTrustedAgentMessageDispatcher,
11901
11921
  executionAuthorityRegistry,
11922
+ getReplicaId as getReplicaId6,
11902
11923
  getStoreLattice as getStoreLattice17,
11903
11924
  ManagedTaskStartError,
11904
11925
  modelLatticeManager as modelLatticeManager3,
@@ -12663,6 +12684,12 @@ var ProjectTaskExecutionService = class {
12663
12684
  return room;
12664
12685
  }
12665
12686
  async requireMembership(task, project, room) {
12687
+ if (isRosterExemptProject(project) && this.deps.ensurePublicOwner) {
12688
+ try {
12689
+ await this.deps.ensurePublicOwner({ tenantId: task.tenantId, projectId: task.projectId, assistantId: task.ownerId });
12690
+ } catch {
12691
+ }
12692
+ }
12666
12693
  const found = snapshotBotMembership(await this.deps.bots.findByAssistant(
12667
12694
  task.tenantId,
12668
12695
  task.projectId,
@@ -16141,6 +16168,42 @@ var ModelProviderService = class {
16141
16168
  }
16142
16169
  return registered;
16143
16170
  }
16171
+ /**
16172
+ * Bring this replica's registry in line with one provider's current state.
16173
+ *
16174
+ * @param tenantId Tenant scope (only the default tenant registers).
16175
+ * @param providerId Provider to reconcile; omit to re-hydrate the tenant.
16176
+ * @remarks A missing provider removes every registry key under its
16177
+ * `${providerId}/` prefix; legacy keys registered through `/api/models` are
16178
+ * left untouched.
16179
+ */
16180
+ async reconcileProvider(tenantId, providerId2) {
16181
+ if (!this.shouldRegister(tenantId)) {
16182
+ return;
16183
+ }
16184
+ if (!providerId2) {
16185
+ await this.hydrateRegistry();
16186
+ return;
16187
+ }
16188
+ const provider = await this.deps.providerStore.getProviderById(tenantId, providerId2);
16189
+ if (!provider) {
16190
+ const prefix = `${providerId2}/`;
16191
+ for (const modelKey2 of this.deps.registry.listRegistered(tenantId)) {
16192
+ if (modelKey2.startsWith(prefix)) {
16193
+ this.deps.registry.remove(tenantId, modelKey2);
16194
+ }
16195
+ }
16196
+ return;
16197
+ }
16198
+ const models = await this.deps.providerStore.listModels(tenantId, providerId2);
16199
+ const current = new Set(models.map((model) => toModelKey(providerId2, model.modelId)));
16200
+ for (const modelKey2 of this.deps.registry.listRegistered(tenantId)) {
16201
+ if (modelKey2.startsWith(`${providerId2}/`) && !current.has(modelKey2)) {
16202
+ this.deps.registry.remove(tenantId, modelKey2);
16203
+ }
16204
+ }
16205
+ this.syncRegistry(tenantId, provider, models);
16206
+ }
16144
16207
  /**
16145
16208
  * Create a provider configuration. Does not run discovery.
16146
16209
  */
@@ -16173,7 +16236,9 @@ var ModelProviderService = class {
16173
16236
  id3,
16174
16237
  { ...input, enabled: input.enabled ?? true }
16175
16238
  );
16176
- return this.toPublicEntry(created, 0);
16239
+ const entry = this.toPublicEntry(created, 0);
16240
+ this.notifyChanged(tenantId, id3);
16241
+ return entry;
16177
16242
  } catch (error) {
16178
16243
  throw this.mapStoreError(error);
16179
16244
  }
@@ -16226,6 +16291,7 @@ var ModelProviderService = class {
16226
16291
  }
16227
16292
  const models = await this.deps.providerStore.listModels(tenantId, id3);
16228
16293
  this.syncRegistry(tenantId, updated, models);
16294
+ this.notifyChanged(tenantId, id3);
16229
16295
  return this.toPublicEntry(updated, models.length);
16230
16296
  } catch (error) {
16231
16297
  throw this.mapStoreError(error);
@@ -16243,6 +16309,7 @@ var ModelProviderService = class {
16243
16309
  for (const model of snapshot) {
16244
16310
  this.deps.registry.remove(tenantId, toModelKey(id3, model.modelId));
16245
16311
  }
16312
+ this.notifyChanged(tenantId, id3);
16246
16313
  }
16247
16314
  /**
16248
16315
  * Discover the provider's models and replace its snapshot.
@@ -16320,6 +16387,7 @@ var ModelProviderService = class {
16320
16387
  }
16321
16388
  registered.push(...this.syncRegistry(tenantId, provider, snapshot));
16322
16389
  }
16390
+ this.notifyChanged(tenantId, id3);
16323
16391
  return {
16324
16392
  discovered,
16325
16393
  registered,
@@ -16436,6 +16504,7 @@ var ModelProviderService = class {
16436
16504
  }
16437
16505
  }
16438
16506
  this.syncRegistry(tenantId, provider, models);
16507
+ this.notifyChanged(tenantId, id3);
16439
16508
  return this.toPublicEntry(provider, models.length);
16440
16509
  }
16441
16510
  syncRegistry(tenantId, provider, models) {
@@ -16458,6 +16527,13 @@ var ModelProviderService = class {
16458
16527
  }
16459
16528
  return registered;
16460
16529
  }
16530
+ /** Best-effort local mutation notification; never affects the operation. */
16531
+ notifyChanged(tenantId, providerId2) {
16532
+ try {
16533
+ this.deps.onChanged?.(tenantId, providerId2);
16534
+ } catch {
16535
+ }
16536
+ }
16461
16537
  /**
16462
16538
  * Enable or disable a single discovered model.
16463
16539
  */
@@ -16479,6 +16555,7 @@ var ModelProviderService = class {
16479
16555
  }
16480
16556
  const models = await this.deps.providerStore.listModels(tenantId, id3);
16481
16557
  this.syncRegistry(tenantId, provider, models);
16558
+ this.notifyChanged(tenantId, id3);
16482
16559
  return this.toPublicEntry(provider, models.length);
16483
16560
  }
16484
16561
  /**
@@ -16800,6 +16877,10 @@ var ModelProviderController = class {
16800
16877
  async hydrateRegistry() {
16801
16878
  return this.service.hydrateRegistry();
16802
16879
  }
16880
+ /** Reconciles one provider (or the tenant) after a foreign change. */
16881
+ async reconcileProvider(tenantId, providerId2) {
16882
+ return this.service.reconcileProvider(tenantId, providerId2);
16883
+ }
16803
16884
  /** GET /api/model-providers */
16804
16885
  async listProviders(request, reply) {
16805
16886
  try {
@@ -16971,6 +17052,27 @@ function registerModelProviderRoutes(app2, controller) {
16971
17052
  );
16972
17053
  }
16973
17054
 
17055
+ // src/services/modelProviderSync.ts
17056
+ var MODEL_PROVIDER_CHANGED_TOPIC = "model-provider-changed";
17057
+ async function publishModelProviderChanged(outbox, owner, tenantId, providerId2) {
17058
+ if (!tenantId) return;
17059
+ try {
17060
+ await outbox.append({
17061
+ topic: MODEL_PROVIDER_CHANGED_TOPIC,
17062
+ payload: { tenantId, providerId: providerId2 },
17063
+ originReplicaId: owner
17064
+ });
17065
+ } catch {
17066
+ }
17067
+ }
17068
+ async function handleModelProviderChanged(payload, reconcile) {
17069
+ const parsed = payload;
17070
+ const tenantId = parsed?.tenantId;
17071
+ if (typeof tenantId !== "string" || tenantId.length === 0) return;
17072
+ const providerId2 = typeof parsed?.providerId === "string" && parsed.providerId.length > 0 ? parsed.providerId : void 0;
17073
+ await reconcile(tenantId, providerId2);
17074
+ }
17075
+
16974
17076
  // src/services/collaboration/ProjectRoomService.ts
16975
17077
  import { randomUUID as randomUUID16 } from "crypto";
16976
17078
  var ProjectRoomService = class {
@@ -17214,19 +17316,149 @@ var ProjectRosterService = class {
17214
17316
  *
17215
17317
  * @param actor Authenticated tenant, user, and project scope.
17216
17318
  * @returns Stored memberships, or — for a public (general) project, which has
17217
- * no authored roster — a synthetic active bot per available tenant agent.
17218
- * @remarks Eligibility is a project property: `kind === "public"` treats every
17219
- * available agent as an implicit member; every other kind returns the stored
17220
- * roster unchanged. The synthetic records are never persisted.
17319
+ * no authored roster — stored memberships plus a **virtual** entry per
17320
+ * remaining available tenant agent.
17321
+ * @remarks Read-only. Eligibility is a project property: `kind === "public"`
17322
+ * derives the member set dynamically from the tenant's agents (excluding the
17323
+ * built-in coordinator and personal assistants). Persisted rows (built-in
17324
+ * coordinator + any agent already interacted with) are returned as-is; every
17325
+ * other eligible agent is a virtual entry whose id materializes
17326
+ * just-in-time on the first @mention / assignment / management action
17327
+ * ({@link ensurePublicProjectBot}). Virtual entries are never persisted.
17221
17328
  */
17222
17329
  async list(actor3) {
17223
17330
  await this.authorize(actor3, "read");
17224
17331
  const project = await this.deps.projects.getProjectById(actor3.tenantId, actor3.projectId);
17225
17332
  if (project?.kind !== "public") return this.deps.membershipStore.list(actor3.tenantId, actor3.projectId);
17226
17333
  const room = await this.deps.roomService.ensureMainRoom(actor3);
17334
+ const persisted = await this.deps.membershipStore.list(actor3.tenantId, actor3.projectId);
17335
+ const known = new Set(persisted.map((item) => item.assistantId));
17336
+ const taken = new Set(persisted.map((item) => item.mentionName));
17227
17337
  const agents = await this.deps.agents.list(actor3.tenantId, actor3.userId);
17228
17338
  const now = /* @__PURE__ */ new Date();
17229
- return agents.map((agent) => publicProjectBot(project, room.id, agent, now));
17339
+ const entries = [...persisted];
17340
+ for (const agent of agents) {
17341
+ if (agent.assistantId === ROOM_COORDINATOR_KEY) continue;
17342
+ if (agent.ownerUserId !== void 0) continue;
17343
+ if (known.has(agent.assistantId)) continue;
17344
+ const mentionName = uniqueMentionName(agent.name, taken);
17345
+ taken.add(mentionName);
17346
+ entries.push(virtualPublicProjectBot(project, room, agent, mentionName, now));
17347
+ }
17348
+ return entries;
17349
+ }
17350
+ /**
17351
+ * Just-in-time materializes one public-project agent into a real membership,
17352
+ * presence thread, and binding.
17353
+ *
17354
+ * @param actor Authenticated tenant, user, and public-project scope.
17355
+ * @param assistantId Eligible tenant agent to materialize.
17356
+ * @returns The existing membership, or the newly materialized one.
17357
+ * @remarks Serialized under the Project mutation mutex so concurrent touches
17358
+ * converge on one row; bounded to a single agent (unlike an eager sweep).
17359
+ * Returns an existing row (any status) unchanged, so an explicitly removed
17360
+ * agent is not resurrected.
17361
+ */
17362
+ async ensurePublicProjectBot(actor3, assistantId) {
17363
+ await this.authorize(actor3, "read");
17364
+ return this.withLock(actor3, () => this.materializePublicBot(actor3, assistantId));
17365
+ }
17366
+ /**
17367
+ * Resolves a roster membership id to a real membership, materializing a
17368
+ * public-project virtual entry just-in-time.
17369
+ *
17370
+ * @param actor Authenticated tenant, user, and project scope.
17371
+ * @param membershipId A real membership id or a public virtual entry id.
17372
+ * @returns The real membership (with a real presence thread).
17373
+ * @remarks Read-capability path used by roster interactions that need an
17374
+ * addressable thread (e.g. "View agent work" on a not-yet-touched agent).
17375
+ * It performs only idempotent writes (materializing/repairing the anchor for
17376
+ * an entry the caller can already read), scoped to this project.
17377
+ */
17378
+ async ensureBotMembership(actor3, membershipId) {
17379
+ await this.authorize(actor3, "read");
17380
+ return this.withLock(actor3, async () => {
17381
+ const existing = await this.deps.membershipStore.findById(actor3.tenantId, membershipId);
17382
+ if (existing && existing.tenantId === actor3.tenantId && existing.projectId === actor3.projectId) {
17383
+ await this.healBinding(existing);
17384
+ return existing;
17385
+ }
17386
+ const assistantId = publicProjectBotAssistantId(actor3.projectId, membershipId);
17387
+ if (assistantId !== void 0) return this.materializePublicBot(actor3, assistantId);
17388
+ throw new ProjectRosterServiceError("ROSTER_MEMBER_NOT_FOUND", 404, "Roster member not found");
17389
+ });
17390
+ }
17391
+ /**
17392
+ * System (trusted) just-in-time materialization of a public-project agent.
17393
+ *
17394
+ * @param input Exact tenant, project, and eligible shared agent.
17395
+ * @returns The existing or newly materialized membership.
17396
+ * @remarks For trusted gateway paths that are not a human capability check
17397
+ * (e.g. starting a public-project task owned by this agent, so it has a room
17398
+ * anchor to post/reply). No human access check is performed; the owner must
17399
+ * be a shared tenant agent (personal assistants are rejected).
17400
+ */
17401
+ async ensurePublicProjectAgent(input) {
17402
+ const actor3 = { tenantId: input.tenantId, userId: "", projectId: input.projectId };
17403
+ return this.withLock(actor3, () => this.materializePublicBot(actor3, input.assistantId));
17404
+ }
17405
+ /** Unlocked single-agent materialization; callers must hold the Project lock. */
17406
+ async materializePublicBot(actor3, assistantId) {
17407
+ const project = await this.deps.projects.getProjectById(actor3.tenantId, actor3.projectId);
17408
+ if (project?.kind !== "public") throw new ProjectRosterServiceError("ASSISTANT_NOT_FOUND", 404, "Public project not found");
17409
+ const existing = await this.deps.membershipStore.findByAssistant(actor3.tenantId, actor3.projectId, assistantId);
17410
+ if (existing) {
17411
+ this.validateMembership(existing, actor3, { id: existing.roomId, workspaceId: existing.workspaceId }, assistantId, existing.roomThreadId);
17412
+ await this.healBinding(existing);
17413
+ return existing;
17414
+ }
17415
+ const agent = (await this.deps.agents.list(actor3.tenantId, actor3.userId)).find((item) => item.assistantId === assistantId);
17416
+ if (!agent || agent.ownerUserId !== void 0) throw new ProjectRosterServiceError("ASSISTANT_NOT_FOUND", 404, "Agent not found");
17417
+ const room = await this.deps.roomService.ensureMainRoom(actor3);
17418
+ const taken = new Set((await this.deps.membershipStore.list(actor3.tenantId, actor3.projectId)).map((item) => item.mentionName));
17419
+ const mentionName = uniqueMentionName(agent.name, taken);
17420
+ let ids = await this.findSafePresence(actor3, room, assistantId);
17421
+ for (let attempt = 0; attempt < 3; attempt += 1) {
17422
+ try {
17423
+ await this.ensureThread(actor3, room, assistantId, ids.membershipId, ids.threadId);
17424
+ break;
17425
+ } catch (error) {
17426
+ if (!(error instanceof ProjectRosterServiceError) || error.code !== "THREAD_CONFLICT") throw error;
17427
+ ids = { membershipId: this.idFactory(), threadId: this.idFactory() };
17428
+ if (attempt === 2) throw error;
17429
+ }
17430
+ }
17431
+ let result;
17432
+ try {
17433
+ result = await this.deps.membershipStore.save({
17434
+ id: ids.membershipId,
17435
+ tenantId: actor3.tenantId,
17436
+ workspaceId: room.workspaceId,
17437
+ projectId: actor3.projectId,
17438
+ roomId: room.id,
17439
+ assistantId,
17440
+ role: "specialist",
17441
+ title: publicBotTitle(agent.name, assistantId),
17442
+ mentionName,
17443
+ status: "active",
17444
+ roomThreadId: ids.threadId
17445
+ });
17446
+ } catch (error) {
17447
+ if (error instanceof Error && error.name === "ProjectBotMembershipIdConflictError") {
17448
+ const raced = await this.deps.membershipStore.findByAssistant(actor3.tenantId, actor3.projectId, assistantId);
17449
+ if (raced) return raced;
17450
+ }
17451
+ throw error;
17452
+ }
17453
+ if (result.kind !== "created" && result.kind !== "updated" && result.kind !== "reactivated") {
17454
+ const raced = await this.deps.membershipStore.findByAssistant(actor3.tenantId, actor3.projectId, assistantId);
17455
+ if (raced) return raced;
17456
+ throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, `Roster ${result.kind.replace("_", " ")}`);
17457
+ }
17458
+ this.validateMembership(result.membership, actor3, room, assistantId, ids.threadId);
17459
+ await this.deps.bindingLifecycle.ensure(result.membership);
17460
+ this.publish(result.membership, result.kind === "created" ? "added" : "updated", `roster:${result.membership.id}:${result.membership.updatedAt.toISOString()}`);
17461
+ return result.membership;
17230
17462
  }
17231
17463
  /** Adds or reactivates a bot and ensures its room-presence thread exists. */
17232
17464
  async add(actor3, input) {
@@ -17255,7 +17487,7 @@ var ProjectRosterService = class {
17255
17487
  try {
17256
17488
  result = await this.deps.membershipStore.save({ id: ids.membershipId, tenantId: actor3.tenantId, workspaceId: room.workspaceId, projectId: actor3.projectId, roomId: room.id, assistantId: input.assistantId, role: input.role, title: normalized.title, ...normalized.responsibility === void 0 ? {} : { responsibility: normalized.responsibility }, mentionName: normalized.mentionName, status: "active", roomThreadId: ids.threadId });
17257
17489
  } catch (error) {
17258
- if (error instanceof Error && ["ProjectBotMembershipIdConflictError", "DuplicateProjectBotMembershipError"].includes(error.name)) throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership conflict");
17490
+ if (error instanceof Error && error.name === "ProjectBotMembershipIdConflictError") throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership conflict");
17259
17491
  throw error;
17260
17492
  }
17261
17493
  if (result.kind !== "created" && result.kind !== "updated" && result.kind !== "reactivated") throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, `Roster ${result.kind.replace("_", " ")}`);
@@ -17282,8 +17514,9 @@ var ProjectRosterService = class {
17282
17514
  async remove(actor3, input) {
17283
17515
  return this.withLock(actor3, async () => {
17284
17516
  await this.authorize(actor3, "manage");
17285
- const target = await this.target(actor3, input.membershipId);
17286
- this.requireCurrent(target, input.expectedUpdatedAt);
17517
+ const { membership: target, materialized } = await this.target(actor3, input.membershipId);
17518
+ const expected = materialized ? target.updatedAt : input.expectedUpdatedAt;
17519
+ this.requireCurrent(target, expected);
17287
17520
  if (target.role === "coordinator") throw coordinatorProtected("The built-in room coordinator cannot be removed");
17288
17521
  for (const status of activeTaskStatuses) {
17289
17522
  const tasks = await this.deps.taskStore.list({ tenantId: actor3.tenantId, projectId: actor3.projectId, ownerType: "agent", ownerId: target.assistantId, status, limit: 1 });
@@ -17292,7 +17525,7 @@ var ProjectRosterService = class {
17292
17525
  await this.authorize(actor3, "manage");
17293
17526
  await this.deps.bindingLifecycle.remove(target);
17294
17527
  try {
17295
- const removed = await this.applyUpdate(actor3, target, { status: "removed" }, input.expectedUpdatedAt);
17528
+ const removed = await this.applyUpdate(actor3, target, { status: "removed" }, expected);
17296
17529
  this.publish(removed, "removed", `roster:${removed.id}:${removed.updatedAt.toISOString()}`);
17297
17530
  return removed;
17298
17531
  } catch (error) {
@@ -17306,11 +17539,12 @@ var ProjectRosterService = class {
17306
17539
  async mutate(actor3, id3, expected, patch, lifecycle) {
17307
17540
  return this.withLock(actor3, async () => {
17308
17541
  await this.authorize(actor3, "manage");
17309
- const target = await this.target(actor3, id3);
17310
- this.requireCurrent(target, expected);
17542
+ const { membership: target, materialized } = await this.target(actor3, id3);
17543
+ const expectedAt = materialized ? target.updatedAt : expected;
17544
+ this.requireCurrent(target, expectedAt);
17311
17545
  this.assertCoordinatorPolicy(target, patch, lifecycle);
17312
17546
  await this.authorize(actor3, "manage");
17313
- const updated = await this.applyUpdate(actor3, target, patch, expected);
17547
+ const updated = await this.applyUpdate(actor3, target, patch, expectedAt);
17314
17548
  if (lifecycle === "pause") await this.deps.bindingLifecycle.pause(updated);
17315
17549
  else if (lifecycle === "resume") await this.deps.bindingLifecycle.resume(updated);
17316
17550
  else await this.applyLifecycle(updated);
@@ -17351,12 +17585,20 @@ var ProjectRosterService = class {
17351
17585
  }
17352
17586
  await this.deps.bindingLifecycle.ensure(membership);
17353
17587
  }
17588
+ /** Best-effort, idempotent binding repair for an already-materialized active member. */
17589
+ async healBinding(membership) {
17590
+ if (membership.status !== "active") return;
17591
+ try {
17592
+ await this.deps.bindingLifecycle.ensure(membership);
17593
+ } catch {
17594
+ }
17595
+ }
17354
17596
  async applyUpdate(actor3, target, patch, expectedUpdatedAt2) {
17355
17597
  let result;
17356
17598
  try {
17357
17599
  result = await this.deps.membershipStore.update({ tenantId: actor3.tenantId, id: target.id, patch, expectedUpdatedAt: expectedUpdatedAt2 });
17358
17600
  } catch (error) {
17359
- if (error instanceof Error && ["ProjectBotMembershipIdConflictError", "DuplicateProjectBotMembershipError"].includes(error.name)) throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership conflict");
17601
+ if (error instanceof Error && error.name === "ProjectBotMembershipIdConflictError") throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership conflict");
17360
17602
  throw error;
17361
17603
  }
17362
17604
  if (result.kind === "updated") {
@@ -17371,7 +17613,18 @@ var ProjectRosterService = class {
17371
17613
  await this.deps.accessService.requireProjectAccess({ ...actor3, capability: capability2 });
17372
17614
  }
17373
17615
  async target(actor3, id3) {
17374
- const target = await this.deps.membershipStore.findById(actor3.tenantId, id3);
17616
+ let target = await this.deps.membershipStore.findById(actor3.tenantId, id3);
17617
+ let materialized = false;
17618
+ if (!target || target.tenantId !== actor3.tenantId || target.projectId !== actor3.projectId) {
17619
+ const assistantId = publicProjectBotAssistantId(actor3.projectId, id3);
17620
+ if (assistantId !== void 0) {
17621
+ const project = await this.deps.projects.getProjectById(actor3.tenantId, actor3.projectId);
17622
+ if (project?.kind === "public") {
17623
+ target = await this.materializePublicBot(actor3, assistantId);
17624
+ materialized = true;
17625
+ }
17626
+ }
17627
+ }
17375
17628
  if (!target || target.tenantId !== actor3.tenantId || target.projectId !== actor3.projectId) throw new ProjectRosterServiceError("ROSTER_MEMBER_NOT_FOUND", 404, "Roster member not found");
17376
17629
  const room = await this.deps.roomService.ensureMainRoom(actor3);
17377
17630
  const assistant = await this.deps.assistantStore.getAssistantById(actor3.tenantId, target.assistantId);
@@ -17381,7 +17634,7 @@ var ProjectRosterService = class {
17381
17634
  const thread = await this.deps.threadStore.getThreadById(actor3.tenantId, target.roomThreadId);
17382
17635
  if (!thread) throw new ProjectRosterServiceError("THREAD_CONFLICT", 409, "Presence thread not found");
17383
17636
  this.validateThread(thread, actor3.tenantId, this.expectedMetadata(room, actor3, target.id, target.assistantId), target.roomThreadId);
17384
- return target;
17637
+ return { membership: target, materialized };
17385
17638
  }
17386
17639
  requireCurrent(target, expected) {
17387
17640
  if (!activeStatuses.includes(target.status)) throw new ProjectRosterServiceError("ROSTER_MEMBER_NOT_FOUND", 404, "Roster member not found");
@@ -17452,22 +17705,50 @@ var ProjectRosterService = class {
17452
17705
  );
17453
17706
  }
17454
17707
  };
17455
- function publicProjectBot(project, roomId, agent, now) {
17456
- const id3 = `public-project-bot:${project.id}:${agent.assistantId}`;
17708
+ function uniqueMentionName(name, taken) {
17709
+ let base = (name || "").toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
17710
+ if (!/^[a-z]/.test(base)) base = `agent-${base}`.replace(/^agent-$/, "agent");
17711
+ base = base.slice(0, 32);
17712
+ if (base.length < 2) base = "agent";
17713
+ let candidate = base;
17714
+ let suffix = 2;
17715
+ while (taken.has(candidate)) {
17716
+ const tail = `-${suffix}`;
17717
+ candidate = `${base.slice(0, 32 - tail.length)}${tail}`;
17718
+ suffix += 1;
17719
+ }
17720
+ return candidate;
17721
+ }
17722
+ function publicBotTitle(name, fallbackId) {
17723
+ const trimmed = (name || "").trim();
17724
+ if (trimmed.length === 0) return fallbackId.slice(0, 80);
17725
+ return trimmed.slice(0, 80);
17726
+ }
17727
+ var PUBLIC_PROJECT_BOT_PREFIX = "public-project-bot:";
17728
+ function publicProjectBotId(projectId, assistantId) {
17729
+ return `${PUBLIC_PROJECT_BOT_PREFIX}${projectId}:${assistantId}`;
17730
+ }
17731
+ function publicProjectBotAssistantId(projectId, membershipId) {
17732
+ const prefix = `${PUBLIC_PROJECT_BOT_PREFIX}${projectId}:`;
17733
+ return membershipId.startsWith(prefix) && membershipId.length > prefix.length ? membershipId.slice(prefix.length) : void 0;
17734
+ }
17735
+ function virtualPublicProjectBot(project, room, agent, mentionName, now) {
17736
+ const id3 = publicProjectBotId(project.id, agent.assistantId);
17457
17737
  return {
17458
17738
  id: id3,
17459
17739
  tenantId: project.tenantId,
17460
17740
  workspaceId: project.workspaceId,
17461
17741
  projectId: project.id,
17462
- roomId,
17742
+ roomId: room.id,
17463
17743
  assistantId: agent.assistantId,
17464
17744
  role: "specialist",
17465
- title: agent.name,
17466
- mentionName: agent.name,
17745
+ title: publicBotTitle(agent.name, agent.assistantId),
17746
+ mentionName,
17467
17747
  status: "active",
17468
17748
  roomThreadId: id3,
17469
17749
  joinedAt: now,
17470
- updatedAt: now
17750
+ updatedAt: now,
17751
+ provisional: true
17471
17752
  };
17472
17753
  }
17473
17754
 
@@ -17588,7 +17869,8 @@ var RoomMessageService = class {
17588
17869
  const room = await this.room(actor3, access.project);
17589
17870
  return this.persistNormalizedHumanMessage(actor3, snapshot, room);
17590
17871
  }
17591
- async persistNormalizedHumanMessage(actor3, snapshot, room) {
17872
+ async persistNormalizedHumanMessage(actor3, input, room) {
17873
+ const snapshot = await this.resolvePublicMentions(actor3, input);
17592
17874
  for (const membershipId of snapshot.botIds) await this.validateMentionIdentity(actor3, room, membershipId);
17593
17875
  let reply = null;
17594
17876
  if (snapshot.replyToMessageId !== void 0) {
@@ -17615,6 +17897,38 @@ var RoomMessageService = class {
17615
17897
  }
17616
17898
  return { message: validated, replayed };
17617
17899
  }
17900
+ /**
17901
+ * Rewrites public-project virtual mention ids to their real membership ids,
17902
+ * materializing each mentioned agent just-in-time.
17903
+ *
17904
+ * @param actor Authenticated sender.
17905
+ * @param snapshot Normalized message with possibly-virtual bot mention ids.
17906
+ * @returns The snapshot unchanged, or one whose bot mentions carry real ids.
17907
+ */
17908
+ async resolvePublicMentions(actor3, snapshot) {
17909
+ const resolver = this.deps.resolveMentionTarget;
17910
+ if (!resolver || snapshot.botIds.length === 0) return snapshot;
17911
+ let changed = false;
17912
+ const seen = /* @__PURE__ */ new Set();
17913
+ const mentions = [];
17914
+ for (const mention of snapshot.mentions) {
17915
+ if (mention.type !== "bot") {
17916
+ mentions.push(mention);
17917
+ continue;
17918
+ }
17919
+ const real = await resolver(actor3, actor3.projectId, mention.membershipId);
17920
+ if (real !== mention.membershipId) changed = true;
17921
+ if (seen.has(real)) {
17922
+ changed = true;
17923
+ continue;
17924
+ }
17925
+ seen.add(real);
17926
+ mentions.push({ type: "bot", membershipId: real });
17927
+ }
17928
+ if (!changed) return snapshot;
17929
+ const botIds = mentions.filter((mention) => mention.type === "bot").map((mention) => mention.membershipId);
17930
+ return { ...snapshot, mentions, botIds };
17931
+ }
17618
17932
  /**
17619
17933
  * Persists a human message, then resolves and dispatches its current targets.
17620
17934
  * Idempotent outcomes are single-flight and retained only in this service process; a
@@ -17853,6 +18167,12 @@ var RoomInterruptService = class {
17853
18167
  throw new RoomInterruptServiceError("ROOM_INTERRUPT_NOT_PENDING", 409, "Room interrupt is no longer pending");
17854
18168
  }
17855
18169
  const inputMessageId = this.idFactory();
18170
+ let rosterExempt = false;
18171
+ try {
18172
+ rosterExempt = isRosterExemptProject(await this.deps.projects.getProjectById(membership.tenantId, membership.projectId));
18173
+ } catch {
18174
+ rosterExempt = false;
18175
+ }
17856
18176
  await this.deps.dispatcher.dispatch(
17857
18177
  {
17858
18178
  tenant_id: scope.tenantId,
@@ -17877,7 +18197,8 @@ var RoomInterruptService = class {
17877
18197
  role: membership.role,
17878
18198
  title: membership.title,
17879
18199
  source: "project_room",
17880
- ...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility }
18200
+ ...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility },
18201
+ ...rosterExempt ? { rosterExempt: true } : {}
17881
18202
  }
17882
18203
  }
17883
18204
  }
@@ -17902,7 +18223,7 @@ var createRoomMessageSchema = z10.object({
17902
18223
  mentions: z10.array(z10.union([
17903
18224
  z10.object({ type: z10.literal("team") }).strict(),
17904
18225
  z10.object({ type: z10.literal("bot"), membershipId: id2 }).strict()
17905
- ])).optional(),
18226
+ ])).max(50).optional(),
17906
18227
  replyToMessageId: id2.optional()
17907
18228
  }).strict();
17908
18229
  var addProjectMemberSchema = z10.object({ userId: id2, role: z10.enum(["owner", "admin", "member", "viewer"]) }).strict();
@@ -18061,6 +18382,12 @@ function registerProjectRosterRoutes(app2, deps) {
18061
18382
  const body = mutationExpectedUpdatedAtSchema.parse(request.body);
18062
18383
  return { data: await method.call(deps.service, getActor(request, p.projectId), { membershipId: p.membershipId, expectedUpdatedAt: body.expectedUpdatedAt }) };
18063
18384
  }));
18385
+ app2.post("/api/projects/:projectId/bots/:membershipId/ensure", async (request, reply) => execute(reply, async () => {
18386
+ const p = membershipParamsSchema.parse(request.params);
18387
+ emptyObjectSchema.parse(request.body ?? {});
18388
+ emptyObjectSchema.parse(request.query ?? {});
18389
+ return { data: await deps.service.ensureBotMembership(getActor(request, p.projectId), p.membershipId) };
18390
+ }));
18064
18391
  app2.delete("/api/projects/:projectId/bots/:membershipId", async (request, reply) => execute(reply, async () => {
18065
18392
  const p = membershipParamsSchema.parse(request.params);
18066
18393
  const q = mutationExpectedUpdatedAtSchema.parse(request.query);
@@ -18968,6 +19295,12 @@ var RoomDispatchService = class {
18968
19295
  if (!source) throw new Error("Invalid project room dispatch message");
18969
19296
  const anchorMessageId = source.sourceId ?? source.id;
18970
19297
  const entries = await this.deps.resolver.resolve(source);
19298
+ let rosterExempt = false;
19299
+ try {
19300
+ rosterExempt = isRosterExemptProject(await this.deps.projects.getProjectById(source.tenantId, source.projectId));
19301
+ } catch {
19302
+ rosterExempt = false;
19303
+ }
18971
19304
  const results = [];
18972
19305
  for (const entry of entries) {
18973
19306
  if (entry.kind === "failure") {
@@ -19007,7 +19340,8 @@ var RoomDispatchService = class {
19007
19340
  role: membership.role,
19008
19341
  title: membership.title,
19009
19342
  source: "project_room",
19010
- ...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility }
19343
+ ...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility },
19344
+ ...rosterExempt ? { rosterExempt: true } : {}
19011
19345
  } }
19012
19346
  }
19013
19347
  });
@@ -22370,6 +22704,14 @@ async function transferGovernedDispatchOwner(task, options, deps) {
22370
22704
  return result.task;
22371
22705
  });
22372
22706
  }
22707
+ function broadcastModelProviderChanged(tenantId, providerId2) {
22708
+ if (!tenantId) return;
22709
+ try {
22710
+ const outbox = getStoreLattice17("default", "outbox").store;
22711
+ void publishModelProviderChanged(outbox, getReplicaId6(), tenantId, providerId2);
22712
+ } catch {
22713
+ }
22714
+ }
22373
22715
  var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22374
22716
  const webAppStore = getStoreLattice17("default", "agentWebApp").store;
22375
22717
  const assistantStore = getStoreLattice17("default", "assistant").store;
@@ -22485,7 +22827,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22485
22827
  });
22486
22828
  return String(await agent.getRunStatus());
22487
22829
  },
22488
- dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope))
22830
+ dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope)),
22831
+ projects: projectStore
22489
22832
  });
22490
22833
  const membershipService = new ProjectMembershipService({ membershipStore: projectMembershipStore, userTenantLinkStore, accessService, projectMutationMutex: projectMutationMutex2, roomStore: projectRoomStore, realtime, onRealtimeFailure: realtime && channelDeps?.warn ? ({ code, membershipId }) => channelDeps.warn?.("project-room:membership-realtime-failed", { code, membershipId }) : void 0 });
22491
22834
  const rosterService = new ProjectRosterService({
@@ -22493,7 +22836,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22493
22836
  roomService,
22494
22837
  membershipStore: projectBotMembershipStore,
22495
22838
  projects: projectStore,
22496
- agents: { list: async (tenantId, userId) => (await collectTenantAssistants(tenantId, userId)).map((assistant) => ({ assistantId: assistant.id, name: assistant.name })) },
22839
+ agents: { list: async (tenantId, userId) => (await collectTenantAssistants(tenantId, userId)).map((assistant) => ({ assistantId: assistant.id, name: assistant.name, ...assistant.ownerUserId === void 0 ? {} : { ownerUserId: assistant.ownerUserId } })) },
22497
22840
  threadStore,
22498
22841
  assistantStore,
22499
22842
  taskStore,
@@ -22507,7 +22850,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22507
22850
  resolver: new RoomTargetResolver({ botStore: projectBotMembershipStore }),
22508
22851
  installation,
22509
22852
  bindings: bindingLifecycle,
22510
- router: channelDeps.router
22853
+ router: channelDeps.router,
22854
+ projects: projectStore
22511
22855
  }) : new UnavailableRoomDispatcher();
22512
22856
  const coordinatorWakeNotifier = dispatcher instanceof RoomDispatchService ? new CoordinatorWakeNotifier({
22513
22857
  tasks: taskStore,
@@ -22527,6 +22871,12 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22527
22871
  botStore: projectBotMembershipStore,
22528
22872
  messageStore: projectRoomMessageStore,
22529
22873
  dispatcher,
22874
+ resolveMentionTarget: async (actor3, projectId, membershipId) => {
22875
+ const assistantId = publicProjectBotAssistantId(projectId, membershipId);
22876
+ if (assistantId === void 0) return membershipId;
22877
+ const membership = await rosterService.ensurePublicProjectBot({ tenantId: actor3.tenantId, userId: actor3.userId, projectId }, assistantId);
22878
+ return membership.id;
22879
+ },
22530
22880
  realtime,
22531
22881
  onRealtimeFailure: realtime && channelDeps?.warn ? (event) => channelDeps.warn?.("project-room:message-realtime-callback-failed", {
22532
22882
  code: event.code,
@@ -22543,6 +22893,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22543
22893
  threadStore,
22544
22894
  realtime,
22545
22895
  log: options.projectTaskLog,
22896
+ ensurePublicOwner: (scope) => rosterService.ensurePublicProjectAgent(scope),
22546
22897
  ...coordinatorWakeNotifier === void 0 ? {} : { coordinatorWake: coordinatorWakeNotifier }
22547
22898
  }) : { reason: void 0 };
22548
22899
  const projectTaskComposition = "composition" in projectTaskRuntime ? projectTaskRuntime.composition : void 0;
@@ -22670,7 +23021,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22670
23021
  has: (tenantId, modelKey2) => modelLatticeManager3.hasLatticeWithTenant(tenantId, modelKey2),
22671
23022
  remove: (tenantId, modelKey2) => modelLatticeManager3.removeLatticeWithTenant(tenantId, modelKey2),
22672
23023
  listRegistered: (tenantId) => modelLatticeManager3.getLatticeKeysByTenant(tenantId)
22673
- }
23024
+ },
23025
+ onChanged: (tenantId, providerId2) => broadcastModelProviderChanged(tenantId, providerId2)
22674
23026
  }, { authRequired: process.env.AUTH_REQUIRED === "true" });
22675
23027
  registerModelProviderRoutes(app2, modelProviderController);
22676
23028
  void modelProviderController.hydrateRegistry().catch(() => void 0);
@@ -23129,7 +23481,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
23129
23481
  return {
23130
23482
  ...projectTaskComposition ? { projectTaskRuntime: projectTaskComposition.lease } : {},
23131
23483
  ...projectTaskUnavailableReason ? { projectTaskUnavailableReason } : {},
23132
- ...projectRoomRealtimeRuntime ? { projectRoomRealtimeRuntime } : {}
23484
+ ...projectRoomRealtimeRuntime ? { projectRoomRealtimeRuntime } : {},
23485
+ modelProviderController
23133
23486
  };
23134
23487
  };
23135
23488
  function tryCreateProjectTaskRuntime(input) {
@@ -23200,20 +23553,24 @@ function createRuntime(input, stores, log2) {
23200
23553
  workspace_id: task.workspaceId,
23201
23554
  project_id: task.projectId
23202
23555
  });
23203
- const execution = new ProjectTaskExecutionService({
23204
- tasks: input.taskStore,
23205
- projects: input.projectStore,
23206
- rooms: input.projectRoomStore,
23207
- bots: input.projectBotMembershipStore,
23208
- threads,
23209
- lifecycle,
23210
- dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope)),
23211
- queue: stores.queue,
23212
- agentFor
23213
- }, {
23214
- settlementTimeoutMs: PROJECT_TASK_SETTLEMENT_TIMEOUT_MS,
23215
- lifecycleSettlementTimeoutMs: PROJECT_TASK_LIFECYCLE_SETTLEMENT_TIMEOUT_MS
23216
- });
23556
+ const execution = new ProjectTaskExecutionService(
23557
+ {
23558
+ tasks: input.taskStore,
23559
+ projects: input.projectStore,
23560
+ rooms: input.projectRoomStore,
23561
+ bots: input.projectBotMembershipStore,
23562
+ threads,
23563
+ lifecycle,
23564
+ dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope)),
23565
+ queue: stores.queue,
23566
+ agentFor,
23567
+ ...input.ensurePublicOwner === void 0 ? {} : { ensurePublicOwner: input.ensurePublicOwner }
23568
+ },
23569
+ {
23570
+ settlementTimeoutMs: PROJECT_TASK_SETTLEMENT_TIMEOUT_MS,
23571
+ lifecycleSettlementTimeoutMs: PROJECT_TASK_LIFECYCLE_SETTLEMENT_TIMEOUT_MS
23572
+ }
23573
+ );
23217
23574
  const observer = new ProjectTaskObserver({
23218
23575
  tasks: input.taskStore,
23219
23576
  workItems: input.taskWorkItemStore,
@@ -25091,6 +25448,40 @@ var _AgentTaskConsumer = class _AgentTaskConsumer {
25091
25448
  _AgentTaskConsumer.agent_run_endpoint = "http://localhost:4001/api/runs";
25092
25449
  var AgentTaskConsumer = _AgentTaskConsumer;
25093
25450
 
25451
+ // src/services/gracefulShutdown.ts
25452
+ function createGracefulShutdown(options) {
25453
+ const timeoutMs = options.timeoutMs ?? 1e4;
25454
+ const exit = options.exit ?? ((code) => process.exit(code));
25455
+ let started = false;
25456
+ let exited = false;
25457
+ const doExit = (code) => {
25458
+ if (exited) return;
25459
+ exited = true;
25460
+ exit(code);
25461
+ };
25462
+ return async (signal) => {
25463
+ if (started) return;
25464
+ started = true;
25465
+ options.log?.info(`Received ${signal}; shutting down gracefully`, { signal });
25466
+ const timer = setTimeout(() => {
25467
+ options.log?.warn("Graceful shutdown timed out; forcing exit", { signal, timeoutMs });
25468
+ doExit(1);
25469
+ }, timeoutMs);
25470
+ timer.unref?.();
25471
+ for (const step of options.steps) {
25472
+ try {
25473
+ await step();
25474
+ } catch (error) {
25475
+ options.log?.warn("Graceful shutdown step failed", {
25476
+ error: error instanceof Error ? error.message : String(error)
25477
+ });
25478
+ }
25479
+ }
25480
+ clearTimeout(timer);
25481
+ doExit(0);
25482
+ };
25483
+ }
25484
+
25094
25485
  // src/index.ts
25095
25486
  import {
25096
25487
  registerLoggerLattice,
@@ -25106,7 +25497,7 @@ import {
25106
25497
  createTrustedAgentMessageDispatcher as createTrustedAgentMessageDispatcher2,
25107
25498
  runAsLeader,
25108
25499
  configureCoordinationLeaseStore,
25109
- getReplicaId as getReplicaId6,
25500
+ getReplicaId as getReplicaId7,
25110
25501
  taskLifecycleNotifier as taskLifecycleNotifier3,
25111
25502
  projectTaskAdmissionNotifier as projectTaskAdmissionNotifier2,
25112
25503
  scheduleLatticeManager as scheduleLatticeManager2,
@@ -25115,7 +25506,9 @@ import {
25115
25506
  agentLatticeManager as agentLatticeManager5,
25116
25507
  configurePluginCatalogSync,
25117
25508
  PLUGIN_CATALOG_CHANGED_TOPIC,
25118
- getSandboxPluginCatalog as getSandboxPluginCatalog3
25509
+ getSandboxPluginCatalog as getSandboxPluginCatalog3,
25510
+ getSandBoxManager as getSandBoxManager4,
25511
+ installPluginHost
25119
25512
  } from "@axiom-lattice/core";
25120
25513
 
25121
25514
  // src/channels/ChannelInfrastructure.ts
@@ -25223,7 +25616,7 @@ function validateRoomTrustedDispatch(message, options, binding, agentId) {
25223
25616
  "inputMessageId",
25224
25617
  "role",
25225
25618
  "title"
25226
- ], ["responsibility", "source"]);
25619
+ ], ["responsibility", "source", "rosterExempt"]);
25227
25620
  if (!binding || !targetEnvelope || !target || !context || !authority || typeof authority.expectedRoomThreadId !== "string" || authority.expectedRoomThreadId.length === 0 || trusted?.executionMode !== "followup") return false;
25228
25621
  const expectedSender = `room:${context.roomId}:membership:${context.membershipId}`;
25229
25622
  return targetEnvelope.adapterChannel === "room" && targetEnvelope.channelInstallationId === message.channelInstallationId && target.sourceRoomMessageId === context.sourceRoomMessageId && target.targetMembershipId === context.membershipId && target.inputMessageId === context.inputMessageId && message.tenantId === context.tenantId && message.sender.id === expectedSender && message.conversation?.id === context.roomId && message.conversation?.type === "group" && binding.tenantId === context.tenantId && binding.channel === "room" && binding.channelInstallationId === message.channelInstallationId && binding.senderId === expectedSender && binding.agentId === context.assistantId && binding.workspaceId === context.workspaceId && binding.projectId === context.projectId && binding.threadId === authority.expectedRoomThreadId && binding.threadMode === "fixed" && agentId === context.assistantId;
@@ -25784,9 +26177,942 @@ async function prepareProjectTaskRuntime(runtime, log2) {
25784
26177
  }
25785
26178
  }
25786
26179
 
26180
+ // src/services/plugins/PluginDeploymentRuntimeStartup.ts
26181
+ import { setPluginDeploymentService } from "@axiom-lattice/core";
26182
+
26183
+ // src/services/plugins/PluginDeploymentService.ts
26184
+ import { randomUUID as randomUUID28 } from "crypto";
26185
+ import {
26186
+ PLUGIN_HOST_DIR,
26187
+ PluginDeploymentRuntimeRegistry
26188
+ } from "@axiom-lattice/core";
26189
+
26190
+ // src/services/plugins/pluginDeploymentNaming.ts
26191
+ import { createHash as createHash10 } from "crypto";
26192
+ var PLUGIN_GUEST_PORT = 8080;
26193
+ var SANDBOX_NAME_HASH_LENGTH = 16;
26194
+ function pluginSandboxName(tenantId, pluginType) {
26195
+ if (!tenantId || typeof tenantId !== "string") {
26196
+ throw new Error("pluginSandboxName: tenantId must be a non-empty string");
26197
+ }
26198
+ if (!pluginType || typeof pluginType !== "string") {
26199
+ throw new Error("pluginSandboxName: pluginType must be a non-empty string");
26200
+ }
26201
+ const hash = createHash10("sha256").update(`${tenantId}\0${pluginType}`).digest("hex").slice(0, SANDBOX_NAME_HASH_LENGTH);
26202
+ return `plugin-${hash}`;
26203
+ }
26204
+
26205
+ // src/services/plugins/PluginDeploymentService.ts
26206
+ var PLUGIN_HOST_SERVER_PATH = `${PLUGIN_HOST_DIR}/server.js`;
26207
+ var DEFAULT_PLUGIN_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
26208
+ var DEFAULT_PLUGIN_HEALTH_TIMEOUT_MS = 3e4;
26209
+ var DEFAULT_PLUGIN_HEALTH_POLL_INTERVAL_MS = 500;
26210
+ var DEFAULT_PLUGIN_HEALTH_PROBE_TIMEOUT_MS = 5e3;
26211
+ var PLUGIN_DEPLOYMENT_LEASE_KEY = "plugin-deployment:reconcile";
26212
+ var PLUGIN_DEPLOYMENT_LEASE_TTL_MS = 6e4;
26213
+ var PLUGIN_TYPE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
26214
+ var PluginDeploymentError = class extends Error {
26215
+ /**
26216
+ * @param code - Machine-readable failure code.
26217
+ * @param message - Human-readable description.
26218
+ */
26219
+ constructor(code, message) {
26220
+ super(message);
26221
+ this.name = "PluginDeploymentError";
26222
+ this.code = code;
26223
+ }
26224
+ };
26225
+ function isRecord4(value) {
26226
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26227
+ }
26228
+ function buildBootstrapEntrypoint(pluginType) {
26229
+ return [
26230
+ "sh",
26231
+ "-c",
26232
+ `while [ ! -f ${PLUGIN_HOST_SERVER_PATH} ]; do sleep 0.2; done; exec node ${PLUGIN_HOST_SERVER_PATH} --type ${pluginType} --port ${PLUGIN_GUEST_PORT}`
26233
+ ];
26234
+ }
26235
+ var PluginDeploymentService = class {
26236
+ /**
26237
+ * @param deps - Injected store, sandbox manager, catalog, installer, lease, and seams.
26238
+ */
26239
+ constructor(deps) {
26240
+ this.store = deps.store;
26241
+ this.sandboxManager = deps.sandboxManager;
26242
+ this.catalog = deps.catalog;
26243
+ this.installPluginHost = deps.installPluginHost;
26244
+ this.lease = deps.lease;
26245
+ this.registry = deps.registry ?? new PluginDeploymentRuntimeRegistry({ now: deps.clock ?? Date.now });
26246
+ this.now = deps.clock ?? Date.now;
26247
+ this.logger = deps.logger ?? { info: () => void 0 };
26248
+ this.limits = deps.limits ?? {};
26249
+ this.serviceBaseUrl = (deps.serviceBaseUrl ?? process.env.MICROSANDBOX_SERVICE_URL ?? "").replace(
26250
+ /\/$/,
26251
+ ""
26252
+ );
26253
+ this.serviceKey = deps.serviceKey;
26254
+ this.fetchImpl = deps.fetchImpl ?? fetch;
26255
+ this.sleep = deps.sleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
26256
+ this.defaultMode = deps.defaultMode ?? "onDemand";
26257
+ this.defaultIdleTimeoutMs = deps.defaultIdleTimeoutMs ?? DEFAULT_PLUGIN_IDLE_TIMEOUT_MS;
26258
+ this.providerName = deps.providerName ?? "default";
26259
+ this.image = deps.image;
26260
+ this.healthTimeoutMs = deps.healthTimeoutMs ?? DEFAULT_PLUGIN_HEALTH_TIMEOUT_MS;
26261
+ this.healthPollIntervalMs = deps.healthPollIntervalMs ?? DEFAULT_PLUGIN_HEALTH_POLL_INTERVAL_MS;
26262
+ this.healthProbeTimeoutMs = deps.healthProbeTimeoutMs ?? DEFAULT_PLUGIN_HEALTH_PROBE_TIMEOUT_MS;
26263
+ this.replicaId = deps.replicaId ?? String(process.pid);
26264
+ this.listAllDeployments = deps.listAllDeployments;
26265
+ }
26266
+ /**
26267
+ * Provision a resident sandbox for a published tenant plugin.
26268
+ *
26269
+ * Validates the plugin against the tenant catalog, enforces limits, resolves a
26270
+ * deterministic sandbox with the bootstrap entrypoint and exposed guest port,
26271
+ * installs the host server, waits for `/health`, and upserts a `running`
26272
+ * {@link PluginDeployment}. Re-deploying an existing deployment reuses the same
26273
+ * sandbox name and record (idempotent).
26274
+ *
26275
+ * @param tenantId - Tenant that owns the plugin.
26276
+ * @param pluginType - Plugin type (directory key).
26277
+ * @param options - Lifecycle mode and overrides.
26278
+ * @returns The stored deployment record.
26279
+ * @throws PluginDeploymentError with `PLUGIN_NOT_FOUND`, `INVALID_PLUGIN_TYPE`,
26280
+ * `LIMIT_EXCEEDED`, `BACKEND_UNSUPPORTED`, `SANDBOX_CREATE_FAILED`, or
26281
+ * `HEALTH_TIMEOUT`.
26282
+ */
26283
+ async deploy(tenantId, pluginType, options = {}) {
26284
+ const snapshot = await this.catalog.ensureLoaded(tenantId);
26285
+ const manifest = snapshot.manifests[pluginType];
26286
+ if (!manifest) {
26287
+ throw new PluginDeploymentError(
26288
+ "PLUGIN_NOT_FOUND",
26289
+ `plugin "${pluginType}" is not published for tenant "${tenantId}"`
26290
+ );
26291
+ }
26292
+ if (!PLUGIN_TYPE_RE.test(pluginType)) {
26293
+ throw new PluginDeploymentError(
26294
+ "INVALID_PLUGIN_TYPE",
26295
+ `plugin type "${pluginType}" is not a valid kebab-case identifier`
26296
+ );
26297
+ }
26298
+ const existing = await this.store.get(tenantId, pluginType);
26299
+ if (!existing) {
26300
+ await this.assertWithinLimits(tenantId);
26301
+ }
26302
+ const mode = options.mode ?? existing?.mode ?? this.defaultMode;
26303
+ const idleTimeoutMs = options.idleTimeoutMs ?? existing?.idleTimeoutMs ?? this.defaultIdleTimeoutMs;
26304
+ const name = pluginSandboxName(tenantId, pluginType);
26305
+ const config = this.buildSandboxConfig(
26306
+ tenantId,
26307
+ pluginType,
26308
+ mode,
26309
+ idleTimeoutMs,
26310
+ options.image ?? this.image
26311
+ );
26312
+ let sandbox;
26313
+ try {
26314
+ sandbox = await this.sandboxManager.createSandbox(name, config);
26315
+ await this.installPluginHost(sandbox);
26316
+ } catch (error) {
26317
+ throw new PluginDeploymentError(
26318
+ "SANDBOX_CREATE_FAILED",
26319
+ `failed to provision sandbox "${name}": ${errorMessage4(error)}`
26320
+ );
26321
+ }
26322
+ const endpoint = await this.resolveEndpoint(sandbox);
26323
+ const health = await this.waitForHealth(endpoint);
26324
+ if (!health.ok) {
26325
+ const failedAt = this.nowIso();
26326
+ await this.store.upsert(
26327
+ this.buildRecord({
26328
+ id: existing?.id ?? randomUUID28(),
26329
+ tenantId,
26330
+ pluginType,
26331
+ pluginVersion: manifest.version,
26332
+ mode,
26333
+ status: "failed",
26334
+ sandboxName: name,
26335
+ endpoint,
26336
+ idleTimeoutMs,
26337
+ provider: options.provider ?? this.providerName,
26338
+ image: options.image ?? this.image,
26339
+ lastHealthAt: failedAt,
26340
+ lastError: health.error,
26341
+ createdAt: existing?.createdAt
26342
+ })
26343
+ );
26344
+ throw new PluginDeploymentError(
26345
+ "HEALTH_TIMEOUT",
26346
+ `resident host for plugin "${pluginType}" did not become healthy: ${health.error ?? "unknown"}`
26347
+ );
26348
+ }
26349
+ const record = this.buildRecord({
26350
+ id: existing?.id ?? randomUUID28(),
26351
+ tenantId,
26352
+ pluginType,
26353
+ pluginVersion: manifest.version,
26354
+ mode,
26355
+ status: "running",
26356
+ sandboxName: name,
26357
+ endpoint,
26358
+ idleTimeoutMs,
26359
+ provider: options.provider ?? this.providerName,
26360
+ image: options.image ?? this.image,
26361
+ lastHealthAt: this.nowIso(),
26362
+ createdAt: existing?.createdAt
26363
+ });
26364
+ const stored = await this.store.upsert(record);
26365
+ const key = this.registryKey(tenantId, pluginType);
26366
+ this.registry.setEndpoint(key, endpoint);
26367
+ this.registry.markHealthy(key);
26368
+ this.logger.info(
26369
+ { event: "plugin.deployment.deployed", tenantId, pluginType, mode, endpoint },
26370
+ "plugin deployment running"
26371
+ );
26372
+ return stored;
26373
+ }
26374
+ /**
26375
+ * Delete a deployment's sandbox and record.
26376
+ *
26377
+ * Sandbox deletion is best-effort: a provider failure is logged but does not
26378
+ * prevent the record from being removed.
26379
+ *
26380
+ * @param tenantId - Tenant that owns the plugin.
26381
+ * @param pluginType - Plugin type.
26382
+ * @returns Whether a deployment record was removed.
26383
+ */
26384
+ async undeploy(tenantId, pluginType) {
26385
+ const name = pluginSandboxName(tenantId, pluginType);
26386
+ try {
26387
+ await this.sandboxManager.deleteSandbox(name);
26388
+ } catch (error) {
26389
+ this.logger.warn?.(
26390
+ { event: "plugin.deployment.undeploy.sandbox_failed", tenantId, pluginType, error: errorMessage4(error) },
26391
+ "best-effort sandbox delete failed"
26392
+ );
26393
+ }
26394
+ const undeployed = await this.store.delete(tenantId, pluginType);
26395
+ this.registry.clear(this.registryKey(tenantId, pluginType));
26396
+ this.logger.info(
26397
+ { event: "plugin.deployment.undeployed", tenantId, pluginType, undeployed },
26398
+ "plugin deployment removed"
26399
+ );
26400
+ return { undeployed };
26401
+ }
26402
+ /**
26403
+ * Read the durable deployment status and refresh it with a live `/health`.
26404
+ *
26405
+ * When a `running` record exists, a single `/health` probe updates
26406
+ * `lastHealthAt`/`lastError`; the probe never throws.
26407
+ *
26408
+ * @param tenantId - Tenant that owns the plugin.
26409
+ * @param pluginType - Plugin type.
26410
+ * @returns The status view (with `status: "absent"` when no record exists).
26411
+ */
26412
+ async status(tenantId, pluginType) {
26413
+ const record = await this.store.get(tenantId, pluginType);
26414
+ if (!record) {
26415
+ return { status: "absent" };
26416
+ }
26417
+ let lastHealthAt = record.lastHealthAt;
26418
+ let lastError = record.lastError;
26419
+ if (record.status === "running" && record.endpoint) {
26420
+ const health = await this.probeHealth(record.endpoint);
26421
+ const key = this.registryKey(tenantId, pluginType);
26422
+ if (health.ok) {
26423
+ this.registry.markHealthy(key);
26424
+ lastHealthAt = this.nowIso();
26425
+ lastError = void 0;
26426
+ } else {
26427
+ this.registry.markUnhealthy(key, health.error ?? "health probe failed");
26428
+ lastHealthAt = this.nowIso();
26429
+ lastError = health.error ?? "health probe failed";
26430
+ }
26431
+ await this.store.updateStatus(tenantId, pluginType, { lastHealthAt, lastError });
26432
+ }
26433
+ return {
26434
+ status: record.status,
26435
+ endpoint: record.endpoint,
26436
+ mode: record.mode,
26437
+ lastHealthAt,
26438
+ ...lastError === void 0 ? {} : { lastError },
26439
+ sandboxName: record.sandboxName,
26440
+ port: record.port
26441
+ };
26442
+ }
26443
+ /**
26444
+ * List every resident deployment for a tenant.
26445
+ *
26446
+ * Reads the durable store directly (no `/health` probe) and maps each record
26447
+ * to the minimal view the read-only `manage_plugin deployments` MCP App needs.
26448
+ *
26449
+ * @param tenantId - Tenant whose deployments are listed.
26450
+ * @returns One minimal entry per stored deployment.
26451
+ */
26452
+ async list(tenantId) {
26453
+ const records = await this.store.listByTenant(tenantId);
26454
+ return records.map((record) => ({
26455
+ pluginType: record.pluginType,
26456
+ status: record.status,
26457
+ mode: record.mode,
26458
+ ...record.endpoint === void 0 ? {} : { endpoint: record.endpoint },
26459
+ ...record.lastHealthAt === void 0 ? {} : { lastHealthAt: record.lastHealthAt },
26460
+ ...record.lastError === void 0 ? {} : { lastError: record.lastError }
26461
+ }));
26462
+ }
26463
+ /**
26464
+ * Read a deployment's stored record without probing its host.
26465
+ *
26466
+ * The catalog executor uses this to decide whether a plugin tool call should
26467
+ * route to the resident service. Unlike {@link status} it never issues a
26468
+ * `/health` probe and resolves `undefined` when no record exists.
26469
+ *
26470
+ * @param tenantId - Tenant that owns the plugin.
26471
+ * @param pluginType - Plugin type.
26472
+ * @returns The minimal `{ status, endpoint, mode }` view, or `undefined`.
26473
+ */
26474
+ async getDeployment(tenantId, pluginType) {
26475
+ const record = await this.store.get(tenantId, pluginType);
26476
+ if (!record) return void 0;
26477
+ return { status: record.status, endpoint: record.endpoint, mode: record.mode };
26478
+ }
26479
+ /**
26480
+ * Ensure an existing on-demand deployment is running and healthy.
26481
+ *
26482
+ * Paused/stopped deployments are resumed (or re-provisioned when the sandbox
26483
+ * is gone) and their status is refreshed. Concurrent calls for the same plugin
26484
+ * are coalesced by the runtime registry's single-flight.
26485
+ *
26486
+ * @param tenantId - Tenant that owns the plugin.
26487
+ * @param pluginType - Plugin type.
26488
+ */
26489
+ async ensureRunning(tenantId, pluginType) {
26490
+ const record = await this.store.get(tenantId, pluginType);
26491
+ if (!record) return;
26492
+ if (record.status === "running" && record.endpoint) return;
26493
+ const key = this.registryKey(tenantId, pluginType);
26494
+ await this.registry.ensureRunning(key, async () => {
26495
+ const config = this.buildSandboxConfig(
26496
+ tenantId,
26497
+ pluginType,
26498
+ record.mode,
26499
+ record.idleTimeoutMs,
26500
+ record.imageRef ?? this.image
26501
+ );
26502
+ let sandbox;
26503
+ try {
26504
+ sandbox = await this.sandboxManager.getSandbox(record.sandboxName);
26505
+ await sandbox.start();
26506
+ } catch {
26507
+ try {
26508
+ sandbox = await this.sandboxManager.createSandbox(record.sandboxName, config);
26509
+ await this.installPluginHost(sandbox);
26510
+ } catch (error) {
26511
+ throw new PluginDeploymentError(
26512
+ "SANDBOX_CREATE_FAILED",
26513
+ `failed to provision sandbox "${record.sandboxName}": ${errorMessage4(error)}`
26514
+ );
26515
+ }
26516
+ }
26517
+ const endpoint = await this.resolveEndpoint(sandbox);
26518
+ const health = await this.waitForHealth(endpoint);
26519
+ if (!health.ok) {
26520
+ await this.store.updateStatus(tenantId, pluginType, {
26521
+ status: "failed",
26522
+ endpoint,
26523
+ lastHealthAt: this.nowIso(),
26524
+ lastError: health.error ?? "health probe failed"
26525
+ });
26526
+ throw new PluginDeploymentError(
26527
+ "HEALTH_TIMEOUT",
26528
+ `resident host for plugin "${pluginType}" did not become healthy: ${health.error ?? "unknown"}`
26529
+ );
26530
+ }
26531
+ await this.store.updateStatus(tenantId, pluginType, {
26532
+ status: "running",
26533
+ endpoint,
26534
+ lastHealthAt: this.nowIso(),
26535
+ lastError: void 0
26536
+ });
26537
+ this.registry.setEndpoint(key, endpoint);
26538
+ this.registry.markHealthy(key);
26539
+ });
26540
+ }
26541
+ /**
26542
+ * Reconcile the store against the provider at startup.
26543
+ *
26544
+ * Lease-gated and never throws: for each stored deployment it verifies the
26545
+ * sandbox still exists, refreshes the endpoint, and marks missing/unreadable
26546
+ * sandboxes `failed` with a diagnostic. Runs are best-effort.
26547
+ *
26548
+ * @returns A summary of the pass.
26549
+ */
26550
+ async startupReconcile() {
26551
+ if (!await this.acquireLease()) {
26552
+ return { ran: false, checked: 0, refreshed: 0, failed: 0 };
26553
+ }
26554
+ const records = await this.listAll();
26555
+ let refreshed = 0;
26556
+ let failed = 0;
26557
+ for (const record of records) {
26558
+ try {
26559
+ const sandbox = await this.sandboxManager.getSandbox(record.sandboxName);
26560
+ const sandboxStatus = await sandbox.getStatus();
26561
+ if (sandboxStatus === "unknown") {
26562
+ throw new Error("sandbox not found");
26563
+ }
26564
+ const endpoint = typeof sandbox.getEndpoint === "function" ? (await sandbox.getEndpoint(record.port || PLUGIN_GUEST_PORT)).endpoint : record.endpoint;
26565
+ await this.store.updateStatus(record.tenantId, record.pluginType, {
26566
+ status: sandboxStatus === "running" ? "running" : record.status,
26567
+ ...endpoint === void 0 ? {} : { endpoint },
26568
+ lastError: void 0
26569
+ });
26570
+ if (endpoint !== void 0) {
26571
+ this.registry.setEndpoint(this.registryKey(record.tenantId, record.pluginType), endpoint);
26572
+ }
26573
+ refreshed += 1;
26574
+ } catch (error) {
26575
+ failed += 1;
26576
+ this.logger.warn?.(
26577
+ {
26578
+ event: "plugin.deployment.reconcile.failed",
26579
+ tenantId: record.tenantId,
26580
+ pluginType: record.pluginType,
26581
+ sandboxName: record.sandboxName,
26582
+ error: errorMessage4(error)
26583
+ },
26584
+ "stored plugin deployment has no reachable sandbox"
26585
+ );
26586
+ await this.safeUpdateStatus(record.tenantId, record.pluginType, {
26587
+ status: "failed",
26588
+ lastError: `startup reconcile: ${errorMessage4(error)}`
26589
+ });
26590
+ }
26591
+ }
26592
+ this.logger.info(
26593
+ { event: "plugin.deployment.reconcile", checked: records.length, refreshed, failed },
26594
+ "plugin deployment startup reconcile complete"
26595
+ );
26596
+ return { ran: true, checked: records.length, refreshed, failed };
26597
+ }
26598
+ /**
26599
+ * Probe every running deployment's `/health` and refresh its health record.
26600
+ *
26601
+ * Lease-gated and never throws. Unhealthy `alwaysOn` deployments are
26602
+ * re-provisioned best-effort.
26603
+ *
26604
+ * @returns A summary of the pass.
26605
+ */
26606
+ async healthWatchdog() {
26607
+ if (!await this.acquireLease()) {
26608
+ return { ran: false, checked: 0, healthy: 0, unhealthy: 0 };
26609
+ }
26610
+ const records = await this.listAll();
26611
+ let healthy = 0;
26612
+ let unhealthy = 0;
26613
+ for (const record of records) {
26614
+ if (record.status !== "running" || !record.endpoint) continue;
26615
+ const key = this.registryKey(record.tenantId, record.pluginType);
26616
+ const health = await this.probeHealth(record.endpoint);
26617
+ if (health.ok) {
26618
+ healthy += 1;
26619
+ this.registry.markHealthy(key);
26620
+ await this.safeUpdateStatus(record.tenantId, record.pluginType, {
26621
+ lastHealthAt: this.nowIso(),
26622
+ lastError: void 0
26623
+ });
26624
+ continue;
26625
+ }
26626
+ unhealthy += 1;
26627
+ this.registry.markUnhealthy(key, health.error ?? "health probe failed");
26628
+ await this.safeUpdateStatus(record.tenantId, record.pluginType, {
26629
+ lastHealthAt: this.nowIso(),
26630
+ lastError: health.error ?? "health probe failed"
26631
+ });
26632
+ if (record.mode === "alwaysOn") {
26633
+ await this.restartBestEffort(record, key);
26634
+ }
26635
+ }
26636
+ this.logger.info(
26637
+ { event: "plugin.deployment.watchdog", checked: healthy + unhealthy, healthy, unhealthy },
26638
+ "plugin deployment health watchdog complete"
26639
+ );
26640
+ return { ran: true, checked: healthy + unhealthy, healthy, unhealthy };
26641
+ }
26642
+ /**
26643
+ * Best-effort re-provision and health-check of an unhealthy `alwaysOn`
26644
+ * deployment.
26645
+ *
26646
+ * @param record - Deployment to restart.
26647
+ * @param key - Runtime registry key.
26648
+ */
26649
+ async restartBestEffort(record, key) {
26650
+ try {
26651
+ const config = this.buildSandboxConfig(
26652
+ record.tenantId,
26653
+ record.pluginType,
26654
+ record.mode,
26655
+ record.idleTimeoutMs,
26656
+ record.imageRef ?? this.image
26657
+ );
26658
+ let sandbox;
26659
+ try {
26660
+ sandbox = await this.sandboxManager.createSandbox(record.sandboxName, config);
26661
+ await sandbox.start().catch(() => void 0);
26662
+ await this.installPluginHost(sandbox);
26663
+ } catch (error) {
26664
+ throw new PluginDeploymentError(
26665
+ "SANDBOX_CREATE_FAILED",
26666
+ `failed to provision sandbox "${record.sandboxName}": ${errorMessage4(error)}`
26667
+ );
26668
+ }
26669
+ const endpoint = await this.resolveEndpoint(sandbox);
26670
+ const health = await this.waitForHealth(endpoint);
26671
+ if (!health.ok) {
26672
+ await this.safeUpdateStatus(record.tenantId, record.pluginType, {
26673
+ endpoint,
26674
+ lastError: health.error ?? "health probe failed"
26675
+ });
26676
+ return;
26677
+ }
26678
+ await this.safeUpdateStatus(record.tenantId, record.pluginType, {
26679
+ status: "running",
26680
+ endpoint,
26681
+ lastHealthAt: this.nowIso(),
26682
+ lastError: void 0
26683
+ });
26684
+ this.registry.setEndpoint(key, endpoint);
26685
+ this.registry.markHealthy(key);
26686
+ } catch (error) {
26687
+ this.logger.warn?.(
26688
+ {
26689
+ event: "plugin.deployment.watchdog.restart_failed",
26690
+ tenantId: record.tenantId,
26691
+ pluginType: record.pluginType,
26692
+ error: errorMessage4(error)
26693
+ },
26694
+ "best-effort alwaysOn restart failed"
26695
+ );
26696
+ }
26697
+ }
26698
+ /**
26699
+ * Build the sandbox creation config for a plugin deployment.
26700
+ *
26701
+ * @param tenantId - Tenant id.
26702
+ * @param pluginType - Plugin type.
26703
+ * @param mode - Lifecycle strategy.
26704
+ * @param idleTimeoutMs - On-demand idle timeout in milliseconds.
26705
+ * @param image - Optional image override for this provisioning.
26706
+ * @returns The provider-facing config.
26707
+ */
26708
+ buildSandboxConfig(tenantId, pluginType, mode, idleTimeoutMs, image = this.image) {
26709
+ return {
26710
+ assistant_id: "",
26711
+ thread_id: pluginSandboxName(tenantId, pluginType),
26712
+ tenantId,
26713
+ entrypoint: buildBootstrapEntrypoint(pluginType),
26714
+ exposePorts: [PLUGIN_GUEST_PORT],
26715
+ // `alwaysOn` deployments never expire; on-demand deployments are
26716
+ // time-boxed by the service (finalized with the lifecycle work in
26717
+ // Plan 4 Task 6). The provider expects seconds, not milliseconds.
26718
+ timeoutSeconds: mode === "alwaysOn" ? null : Math.max(1, Math.round(idleTimeoutMs / 1e3)),
26719
+ ...image === void 0 ? {} : { image }
26720
+ };
26721
+ }
26722
+ /**
26723
+ * Resolve the routable endpoint for the published guest port.
26724
+ *
26725
+ * @param sandbox - Provisioned sandbox instance.
26726
+ * @returns The relative proxy path or scheme-less host endpoint.
26727
+ * @throws PluginDeploymentError with `BACKEND_UNSUPPORTED` when the provider
26728
+ * does not implement `getEndpoint`.
26729
+ */
26730
+ async resolveEndpoint(sandbox) {
26731
+ if (typeof sandbox.getEndpoint !== "function") {
26732
+ throw new PluginDeploymentError(
26733
+ "BACKEND_UNSUPPORTED",
26734
+ "sandbox provider does not support resident plugin services (missing getEndpoint)"
26735
+ );
26736
+ }
26737
+ const { endpoint } = await sandbox.getEndpoint(PLUGIN_GUEST_PORT);
26738
+ return endpoint;
26739
+ }
26740
+ /**
26741
+ * Poll `/health` until the host answers healthy or the deadline passes.
26742
+ *
26743
+ * @param endpoint - Resolved endpoint route.
26744
+ * @returns The final probe outcome.
26745
+ */
26746
+ async waitForHealth(endpoint) {
26747
+ const deadline = Date.now() + this.healthTimeoutMs;
26748
+ let last = { ok: false, error: "health not attempted" };
26749
+ do {
26750
+ last = await this.probeHealth(endpoint);
26751
+ if (last.ok) return last;
26752
+ if (Date.now() >= deadline) break;
26753
+ await this.sleep(this.healthPollIntervalMs);
26754
+ } while (Date.now() < deadline);
26755
+ return last;
26756
+ }
26757
+ /**
26758
+ * Issue a single `GET ${baseUrl}${endpoint}/health` request.
26759
+ *
26760
+ * Never throws; network/timeout/non-2xx/parse failures resolve `{ ok: false }`.
26761
+ *
26762
+ * @param endpoint - Resolved endpoint route.
26763
+ * @returns The probe outcome.
26764
+ */
26765
+ async probeHealth(endpoint) {
26766
+ const url = `${this.serviceBaseUrl}${endpoint}/health`;
26767
+ const controller = new AbortController();
26768
+ const timer = setTimeout(() => controller.abort(), this.healthProbeTimeoutMs);
26769
+ try {
26770
+ const response = await this.fetchImpl(url, {
26771
+ method: "GET",
26772
+ ...this.serviceKey === void 0 ? {} : { headers: { Authorization: `Bearer ${this.serviceKey}` } },
26773
+ signal: controller.signal
26774
+ });
26775
+ if (!response.ok) {
26776
+ return { ok: false, error: `health responded with HTTP ${response.status}` };
26777
+ }
26778
+ const body = await response.json().catch(() => void 0);
26779
+ if (isRecord4(body) && body.ok === true) {
26780
+ return { ok: true };
26781
+ }
26782
+ return { ok: false, error: "health returned an invalid envelope" };
26783
+ } catch (error) {
26784
+ return { ok: false, error: errorMessage4(error) };
26785
+ } finally {
26786
+ clearTimeout(timer);
26787
+ }
26788
+ }
26789
+ /**
26790
+ * Enforce per-tenant and global deployment caps.
26791
+ *
26792
+ * @param tenantId - Tenant attempting to deploy.
26793
+ * @throws PluginDeploymentError with `LIMIT_EXCEEDED`.
26794
+ */
26795
+ async assertWithinLimits(tenantId) {
26796
+ const { maxPerTenant, maxGlobal } = this.limits;
26797
+ if (maxPerTenant !== void 0) {
26798
+ const tenantRecords = await this.store.listByTenant(tenantId);
26799
+ if (tenantRecords.length >= maxPerTenant) {
26800
+ throw new PluginDeploymentError(
26801
+ "LIMIT_EXCEEDED",
26802
+ `tenant "${tenantId}" reached the resident plugin limit (${maxPerTenant})`
26803
+ );
26804
+ }
26805
+ }
26806
+ if (maxGlobal !== void 0 && this.listAllDeployments) {
26807
+ const all = await this.listAllDeployments();
26808
+ if (all.length >= maxGlobal) {
26809
+ throw new PluginDeploymentError(
26810
+ "LIMIT_EXCEEDED",
26811
+ `global resident plugin limit reached (${maxGlobal})`
26812
+ );
26813
+ }
26814
+ }
26815
+ }
26816
+ /**
26817
+ * Acquire the reconcile lease for this pass.
26818
+ *
26819
+ * @returns True when this replica may run the pass.
26820
+ */
26821
+ async acquireLease() {
26822
+ try {
26823
+ return await this.lease.tryAcquire(
26824
+ PLUGIN_DEPLOYMENT_LEASE_KEY,
26825
+ this.replicaId,
26826
+ PLUGIN_DEPLOYMENT_LEASE_TTL_MS
26827
+ );
26828
+ } catch (error) {
26829
+ this.logger.warn?.(
26830
+ { event: "plugin.deployment.lease.failed", error: errorMessage4(error) },
26831
+ "could not acquire reconcile lease; skipping pass"
26832
+ );
26833
+ return false;
26834
+ }
26835
+ }
26836
+ /**
26837
+ * List deployments for reconcile/watchdog, degrading to an empty list.
26838
+ *
26839
+ * @returns Stored deployments, or `[]` when no global lister is wired.
26840
+ */
26841
+ async listAll() {
26842
+ if (!this.listAllDeployments) return [];
26843
+ try {
26844
+ return await this.listAllDeployments();
26845
+ } catch (error) {
26846
+ this.logger.warn?.(
26847
+ { event: "plugin.deployment.list.failed", error: errorMessage4(error) },
26848
+ "could not list plugin deployments"
26849
+ );
26850
+ return [];
26851
+ }
26852
+ }
26853
+ /**
26854
+ * Apply a status patch, swallowing store errors (used on the soft paths).
26855
+ *
26856
+ * @param tenantId - Tenant id.
26857
+ * @param pluginType - Plugin type.
26858
+ * @param patch - Partial deployment update.
26859
+ */
26860
+ async safeUpdateStatus(tenantId, pluginType, patch) {
26861
+ try {
26862
+ await this.store.updateStatus(tenantId, pluginType, patch);
26863
+ } catch (error) {
26864
+ this.logger.warn?.(
26865
+ {
26866
+ event: "plugin.deployment.store_update_failed",
26867
+ tenantId,
26868
+ pluginType,
26869
+ error: errorMessage4(error)
26870
+ },
26871
+ "could not update plugin deployment status"
26872
+ );
26873
+ }
26874
+ }
26875
+ /**
26876
+ * Assemble a durable deployment record.
26877
+ *
26878
+ * @param input - Record fields (optional fields fall back to now/undefined).
26879
+ * @returns A fully populated deployment.
26880
+ */
26881
+ buildRecord(input) {
26882
+ const now = this.nowIso();
26883
+ return {
26884
+ id: input.id,
26885
+ tenantId: input.tenantId,
26886
+ pluginType: input.pluginType,
26887
+ pluginVersion: input.pluginVersion,
26888
+ mode: input.mode,
26889
+ status: input.status,
26890
+ provider: input.provider,
26891
+ sandboxName: input.sandboxName,
26892
+ port: PLUGIN_GUEST_PORT,
26893
+ endpoint: input.endpoint,
26894
+ ...input.image === void 0 ? {} : { imageRef: input.image },
26895
+ idleTimeoutMs: input.idleTimeoutMs,
26896
+ ...input.lastHealthAt === void 0 ? {} : { lastHealthAt: input.lastHealthAt },
26897
+ ...input.lastError === void 0 ? {} : { lastError: input.lastError },
26898
+ createdAt: input.createdAt ?? now,
26899
+ updatedAt: now
26900
+ };
26901
+ }
26902
+ /**
26903
+ * Compose the runtime registry key for a deployment.
26904
+ *
26905
+ * @param tenantId - Tenant id.
26906
+ * @param pluginType - Plugin type.
26907
+ * @returns `${tenantId}:${pluginType}`.
26908
+ */
26909
+ registryKey(tenantId, pluginType) {
26910
+ return `${tenantId}:${pluginType}`;
26911
+ }
26912
+ /**
26913
+ * Current time as an ISO string from the injected clock.
26914
+ *
26915
+ * @returns ISO-8601 timestamp.
26916
+ */
26917
+ nowIso() {
26918
+ return new Date(this.now()).toISOString();
26919
+ }
26920
+ };
26921
+ function errorMessage4(error) {
26922
+ return error instanceof Error ? error.message : String(error);
26923
+ }
26924
+
26925
+ // src/services/plugins/PluginDeploymentRuntimeStartup.ts
26926
+ var DEFAULT_PLUGIN_WATCHDOG_INTERVAL_MS = 3e4;
26927
+ var DEFAULT_PLUGIN_RECONCILE_DEADLINE_MS = 3e4;
26928
+ var DEFAULT_PLUGIN_MAX_PER_TENANT = 10;
26929
+ var DEFAULT_PLUGIN_MAX_GLOBAL = 100;
26930
+ var PLUGIN_DEPLOYMENT_RUNTIME_FLAG = "AXIOM_PLUGIN_DEPLOYMENT_RUNTIME";
26931
+ var PLUGIN_DEPLOYMENT_MAX_PER_TENANT_ENV = "PLUGIN_DEPLOYMENT_MAX_PER_TENANT";
26932
+ var PLUGIN_DEPLOYMENT_MAX_GLOBAL_ENV = "PLUGIN_DEPLOYMENT_MAX_GLOBAL";
26933
+ function isPluginDeploymentRuntimeEnabled(env = process.env) {
26934
+ const flag = env[PLUGIN_DEPLOYMENT_RUNTIME_FLAG];
26935
+ if (flag === "false" || flag === "0") return false;
26936
+ if (flag === "true" || flag === "1") return true;
26937
+ return Boolean(env.MICROSANDBOX_SERVICE_URL);
26938
+ }
26939
+ function readPluginDeploymentLimits(env = process.env) {
26940
+ return {
26941
+ maxPerTenant: parsePositiveInt(
26942
+ env[PLUGIN_DEPLOYMENT_MAX_PER_TENANT_ENV],
26943
+ DEFAULT_PLUGIN_MAX_PER_TENANT
26944
+ ),
26945
+ maxGlobal: parsePositiveInt(
26946
+ env[PLUGIN_DEPLOYMENT_MAX_GLOBAL_ENV],
26947
+ DEFAULT_PLUGIN_MAX_GLOBAL
26948
+ )
26949
+ };
26950
+ }
26951
+ function startPluginDeploymentRuntime(options) {
26952
+ const { lease, holder, logger: logger7 } = options;
26953
+ const forcedOn = process.env[PLUGIN_DEPLOYMENT_RUNTIME_FLAG] === "true" || process.env[PLUGIN_DEPLOYMENT_RUNTIME_FLAG] === "1";
26954
+ const effectiveServiceBaseUrl = options.wiring.serviceBaseUrl ?? process.env.MICROSANDBOX_SERVICE_URL;
26955
+ if (forcedOn && !effectiveServiceBaseUrl) {
26956
+ logger7.warn(
26957
+ `${PLUGIN_DEPLOYMENT_RUNTIME_FLAG} is enabled but MICROSANDBOX_SERVICE_URL is empty; skipping plugin deployment runtime`,
26958
+ { holder }
26959
+ );
26960
+ return { reconcile: Promise.resolve(), dispose: async () => void 0 };
26961
+ }
26962
+ const register = options.register ?? ((service3) => setPluginDeploymentService(service3));
26963
+ const setIntervalFn = options.setIntervalFn ?? setInterval;
26964
+ const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
26965
+ const watchdogIntervalMs = options.watchdogIntervalMs ?? DEFAULT_PLUGIN_WATCHDOG_INTERVAL_MS;
26966
+ const reconcileDeadlineMs = options.reconcileDeadlineMs ?? DEFAULT_PLUGIN_RECONCILE_DEADLINE_MS;
26967
+ const createService = options.createService ?? ((deps2) => new PluginDeploymentService(deps2));
26968
+ const deps = {
26969
+ ...options.wiring,
26970
+ lease,
26971
+ replicaId: holder
26972
+ };
26973
+ const service2 = createService(deps);
26974
+ register(service2);
26975
+ logger7.info("plugin deployment runtime started", { holder });
26976
+ const reconcile = runStartupReconcile(service2, reconcileDeadlineMs, logger7, holder);
26977
+ let held = false;
26978
+ let inFlight = false;
26979
+ const tick = async () => {
26980
+ if (inFlight) return;
26981
+ inFlight = true;
26982
+ try {
26983
+ if (held) {
26984
+ held = await renewLease(lease, holder, logger7);
26985
+ if (!held) {
26986
+ logger7.warn("plugin deployment lease lost; watchdog paused", { holder });
26987
+ }
26988
+ } else {
26989
+ held = await acquireLease(lease, holder, logger7);
26990
+ if (held) {
26991
+ logger7.info("plugin deployment lease acquired", { holder });
26992
+ }
26993
+ }
26994
+ if (!held) return;
26995
+ await runWatchdog(service2, holder, logger7);
26996
+ } finally {
26997
+ inFlight = false;
26998
+ }
26999
+ };
27000
+ const interval = setIntervalFn(() => {
27001
+ void tick();
27002
+ }, watchdogIntervalMs);
27003
+ return {
27004
+ reconcile,
27005
+ async dispose() {
27006
+ clearIntervalFn(interval);
27007
+ if (held) {
27008
+ try {
27009
+ await lease.release(PLUGIN_DEPLOYMENT_LEASE_KEY, holder);
27010
+ } catch (error) {
27011
+ logger7.warn("plugin deployment lease release failed", {
27012
+ holder,
27013
+ error: errorMessage5(error)
27014
+ });
27015
+ }
27016
+ held = false;
27017
+ }
27018
+ register(void 0);
27019
+ logger7.info("plugin deployment runtime stopped", { holder });
27020
+ }
27021
+ };
27022
+ }
27023
+ async function runStartupReconcile(service2, deadlineMs, logger7, holder) {
27024
+ try {
27025
+ await withDeadline(service2.startupReconcile(), deadlineMs);
27026
+ logger7.info("plugin deployment startup reconcile complete", { holder });
27027
+ } catch (error) {
27028
+ logger7.warn("plugin deployment startup reconcile failed", {
27029
+ holder,
27030
+ error: errorMessage5(error)
27031
+ });
27032
+ }
27033
+ }
27034
+ async function withDeadline(work, deadlineMs) {
27035
+ const settled = work.then(() => void 0);
27036
+ if (!Number.isFinite(deadlineMs) || deadlineMs <= 0) {
27037
+ await settled;
27038
+ return;
27039
+ }
27040
+ let timer;
27041
+ const deadline = new Promise((resolve3) => {
27042
+ timer = setTimeout(resolve3, deadlineMs);
27043
+ unrefTimer(timer);
27044
+ });
27045
+ try {
27046
+ await Promise.race([settled, deadline]);
27047
+ } finally {
27048
+ if (timer !== void 0) clearTimeout(timer);
27049
+ }
27050
+ }
27051
+ async function acquireLease(lease, holder, logger7) {
27052
+ try {
27053
+ return await lease.tryAcquire(
27054
+ PLUGIN_DEPLOYMENT_LEASE_KEY,
27055
+ holder,
27056
+ PLUGIN_DEPLOYMENT_LEASE_TTL_MS
27057
+ );
27058
+ } catch (error) {
27059
+ logger7.warn("plugin deployment lease acquire failed", {
27060
+ holder,
27061
+ error: errorMessage5(error)
27062
+ });
27063
+ return false;
27064
+ }
27065
+ }
27066
+ async function renewLease(lease, holder, logger7) {
27067
+ try {
27068
+ return await lease.renew(
27069
+ PLUGIN_DEPLOYMENT_LEASE_KEY,
27070
+ holder,
27071
+ PLUGIN_DEPLOYMENT_LEASE_TTL_MS
27072
+ );
27073
+ } catch (error) {
27074
+ logger7.warn("plugin deployment lease renew failed", {
27075
+ holder,
27076
+ error: errorMessage5(error)
27077
+ });
27078
+ return false;
27079
+ }
27080
+ }
27081
+ async function runWatchdog(service2, holder, logger7) {
27082
+ try {
27083
+ await service2.healthWatchdog();
27084
+ } catch (error) {
27085
+ logger7.warn("plugin deployment health watchdog failed", {
27086
+ holder,
27087
+ error: errorMessage5(error)
27088
+ });
27089
+ }
27090
+ }
27091
+ function unrefTimer(timer) {
27092
+ if (typeof timer === "object" && timer !== null && "unref" in timer) {
27093
+ timer.unref?.();
27094
+ }
27095
+ }
27096
+ function parsePositiveInt(value, fallback) {
27097
+ if (value === void 0 || value.trim() === "") return fallback;
27098
+ const parsed = Number(value);
27099
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
27100
+ return Math.floor(parsed);
27101
+ }
27102
+ function errorMessage5(error) {
27103
+ return error instanceof Error ? error.message : String(error);
27104
+ }
27105
+
25787
27106
  // src/services/multiReplicaReadiness.ts
25788
27107
  import { getStoreLattice as getStoreLattice28 } from "@axiom-lattice/core";
25789
- var SHARED_STORE_KINDS = ["lease", "outbox", "threadMessageQueue", "task", "taskWorkItem"];
27108
+ var SHARED_STORE_KINDS = [
27109
+ "lease",
27110
+ "outbox",
27111
+ "threadMessageQueue",
27112
+ "task",
27113
+ "taskWorkItem",
27114
+ "channelInstallation"
27115
+ ];
25790
27116
  function storeClassName(kind) {
25791
27117
  try {
25792
27118
  return getStoreLattice28("default", kind).store.constructor?.name;
@@ -26058,6 +27384,7 @@ app.setErrorHandler((error, request, reply) => {
26058
27384
  error: error.message || "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF"
26059
27385
  });
26060
27386
  });
27387
+ var shutdownHandlersInstalled = false;
26061
27388
  function getConfiguredSandboxProvider() {
26062
27389
  const sandboxProviderType = process.env.SANDBOX_PROVIDER_TYPE || "microsandbox-remote";
26063
27390
  return createSandboxProvider({
@@ -26191,18 +27518,18 @@ var start = async (config) => {
26191
27518
  projectRoomRealtimeRuntime
26192
27519
  });
26193
27520
  try {
26194
- const { A2AAuthService, parseEnvKeys } = await import("./A2AAuthService-7UGQ7GMW.mjs");
27521
+ const { A2AAuthService: A2AAuthService2, parseEnvKeys: parseEnvKeys2 } = await import("./A2AAuthService-7UGQ7GMW.mjs");
26195
27522
  const { registerA2AStandardRoutes } = await import("./a2a-standard-PBYPNHOA.mjs");
26196
- const a2a = await import("./a2a-FGYX2HH3.mjs");
27523
+ const a2a = await import("./a2a-BHBLACJV.mjs");
26197
27524
  const a2aKeyStore = getStoreLattice29("default", "a2aApiKey").store;
26198
27525
  const assistantStore = getStoreLattice29("default", "assistant").store;
26199
27526
  const projectStore = getStoreLattice29("default", "project").store;
26200
27527
  const taskStore = getStoreLattice29("default", "task").store;
26201
- const authService = new A2AAuthService({
27528
+ const authService = new A2AAuthService2({
26202
27529
  keyStore: a2aKeyStore,
26203
27530
  assistantStore,
26204
27531
  projectStore,
26205
- envKeyMap: parseEnvKeys(process.env.A2A_API_KEYS || ""),
27532
+ envKeyMap: parseEnvKeys2(process.env.A2A_API_KEYS || ""),
26206
27533
  allowUnauthenticated: process.env.A2A_ALLOW_UNAUTHENTICATED === "true"
26207
27534
  });
26208
27535
  await authService.refresh();
@@ -26227,7 +27554,7 @@ var start = async (config) => {
26227
27554
  const { registerOpenCatalogRoutes } = await import("./open-catalog-SZQ435AY.mjs");
26228
27555
  const { registerOpenFilesRoutes } = await import("./open-files-X3HXVEEM.mjs");
26229
27556
  const { registerOpenAuditRoutes } = await import("./open-audit-UJ3NXCGW.mjs");
26230
- const { registerOpenExposureRoutes } = await import("./open-exposure-VU227IO3.mjs");
27557
+ const { registerOpenExposureRoutes } = await import("./open-exposure-27ESFWTB.mjs");
26231
27558
  const { openLattice } = await import("./openLattice-Q6M5S6CY.mjs");
26232
27559
  const { createKbProvider } = await import("./kbProvider-2D6DZ34N.mjs");
26233
27560
  const { createOpenSandboxProvider } = await import("./sandboxProvider-DUYWHYJN.mjs");
@@ -26275,6 +27602,50 @@ var start = async (config) => {
26275
27602
  sandboxLatticeManager2.registerLattice("default", getConfiguredSandboxProvider());
26276
27603
  logger6.info("Registered sandbox manager from env configuration");
26277
27604
  }
27605
+ if (isPluginDeploymentRuntimeEnabled()) {
27606
+ try {
27607
+ const pluginDeploymentStore = getStoreLattice29("default", "pluginDeployment").store;
27608
+ const tenantStore = getStoreLattice29("default", "tenant").store;
27609
+ const leaseStore2 = getStoreLattice29("default", "lease").store;
27610
+ const listAllDeployments = async () => {
27611
+ const tenants = await tenantStore.getAllTenants();
27612
+ const lists = await Promise.all(
27613
+ tenants.map(
27614
+ (tenant2) => pluginDeploymentStore.listByTenant(tenant2.id).catch(() => [])
27615
+ )
27616
+ );
27617
+ return lists.flat();
27618
+ };
27619
+ const pluginDeploymentRuntime = startPluginDeploymentRuntime({
27620
+ wiring: {
27621
+ store: pluginDeploymentStore,
27622
+ sandboxManager: getSandBoxManager4(),
27623
+ catalog: getSandboxPluginCatalog3(),
27624
+ installPluginHost,
27625
+ limits: readPluginDeploymentLimits(),
27626
+ serviceKey: process.env.MICROSANDBOX_API_KEY,
27627
+ listAllDeployments,
27628
+ // The service logs fields-first; the gateway logger is message-first.
27629
+ logger: {
27630
+ info: (fields, message) => logger6.info(message ?? "plugin deployment", fields),
27631
+ warn: (fields, message) => logger6.warn(message ?? "plugin deployment", fields),
27632
+ error: (fields, message) => logger6.error(message ?? "plugin deployment", fields)
27633
+ }
27634
+ },
27635
+ lease: leaseStore2,
27636
+ holder: getReplicaId7(),
27637
+ logger: logger6
27638
+ });
27639
+ app.addHook("onClose", async () => {
27640
+ await pluginDeploymentRuntime.dispose();
27641
+ });
27642
+ logger6.info("Plugin deployment runtime started");
27643
+ } catch (err) {
27644
+ logger6.warn("Plugin deployment runtime unavailable", {
27645
+ error: err instanceof Error ? err.message : String(err)
27646
+ });
27647
+ }
27648
+ }
26278
27649
  try {
26279
27650
  const { ResourceController } = await import("./resources-2L3KSUUU.mjs");
26280
27651
  const sharedResourceStore = getStoreLattice29("default", "sharedResource").store;
@@ -26293,12 +27664,21 @@ var start = async (config) => {
26293
27664
  });
26294
27665
  }
26295
27666
  if (channelDeps && process.env.CHANNELS_ENABLED !== "false") {
26296
- const { connectAllChannels } = await import("@axiom-lattice/core");
27667
+ const { connectAllChannels, disconnectAllChannels, ChannelConnectionLeader } = await import("@axiom-lattice/core");
26297
27668
  try {
26298
- await connectAllChannels(
26299
- (channel) => adapterRegistry.get(channel),
26300
- { deps: { router: channelDeps.router } }
26301
- );
27669
+ const channelLeader = new ChannelConnectionLeader({
27670
+ leaseStore: getStoreLattice29("default", "lease").store,
27671
+ owner: getReplicaId7(),
27672
+ connect: () => connectAllChannels((channel) => adapterRegistry.get(channel), {
27673
+ deps: { router: channelDeps.router }
27674
+ }),
27675
+ disconnect: () => disconnectAllChannels((channel) => adapterRegistry.get(channel)),
27676
+ log: { warn: (message, context) => logger6.warn(message, context) }
27677
+ });
27678
+ app.addHook("onClose", async () => {
27679
+ await channelLeader.stop();
27680
+ });
27681
+ channelLeader.start();
26302
27682
  } catch (err) {
26303
27683
  logger6.error("Failed to start channel connections", {
26304
27684
  error: err instanceof Error ? err.message : String(err)
@@ -26310,20 +27690,20 @@ var start = async (config) => {
26310
27690
  const outboxStore = getStoreLattice29("default", "outbox").store;
26311
27691
  taskLifecycleNotifier3.setOutbox(outboxStore);
26312
27692
  projectTaskAdmissionNotifier2.setOutbox(outboxStore);
26313
- configurePluginCatalogSync({ outbox: outboxStore, owner: getReplicaId6() });
27693
+ configurePluginCatalogSync({ outbox: outboxStore, owner: getReplicaId7() });
26314
27694
  bridgeAssistantEvents(
26315
27695
  (event, cb) => eventBus4.subscribe(event, cb),
26316
27696
  outboxStore,
26317
- getReplicaId6()
27697
+ getReplicaId7()
26318
27698
  );
26319
27699
  if (projectRoomRealtimeRuntime) {
26320
27700
  projectRoomRealtimeRuntime.broker.setBroadcastHook((scope, serialized, sourceKey) => {
26321
- void publishRoomEvent(outboxStore, getReplicaId6(), scope, serialized, sourceKey);
27701
+ void publishRoomEvent(outboxStore, getReplicaId7(), scope, serialized, sourceKey);
26322
27702
  });
26323
27703
  }
26324
27704
  const outboxPoller = new OutboxPoller({
26325
27705
  store: outboxStore,
26326
- owner: getReplicaId6(),
27706
+ owner: getReplicaId7(),
26327
27707
  handlers: {
26328
27708
  "task-lifecycle": (payload) => taskLifecycleNotifier3.deliverFromOutbox(payload),
26329
27709
  "project-task-admission": (payload) => projectTaskAdmissionNotifier2.deliverFromOutbox(payload),
@@ -26331,6 +27711,7 @@ var start = async (config) => {
26331
27711
  if (projectRoomRealtimeRuntime) ingestRoomEvent(payload, projectRoomRealtimeRuntime.broker);
26332
27712
  },
26333
27713
  "assistant-changed": (payload) => handleAssistantChanged(payload, (tenantId) => agentLatticeManager5.initializeStoredAssistantsForTenant(tenantId)),
27714
+ [A2A_KEY_CHANGED_TOPIC]: (payload) => handleA2AKeyChanged(payload, () => refreshStoreKeyMap()),
26334
27715
  [MCP_CONFIG_CHANGED_TOPIC]: (payload) => reconcileMcpConfigChange(payload, {
26335
27716
  store: getStoreLattice29("default", "mcp").store,
26336
27717
  manager: mcpManager3,
@@ -26339,6 +27720,11 @@ var start = async (config) => {
26339
27720
  [PLUGIN_CATALOG_CHANGED_TOPIC]: async (payload) => {
26340
27721
  const tenantId = payload?.tenantId;
26341
27722
  if (typeof tenantId === "string" && tenantId.length > 0) getSandboxPluginCatalog3().invalidate(tenantId);
27723
+ },
27724
+ [MODEL_PROVIDER_CHANGED_TOPIC]: async (payload) => {
27725
+ const controller = routes.modelProviderController;
27726
+ if (!controller) return;
27727
+ await handleModelProviderChanged(payload, (tenantId, providerId2) => controller.reconcileProvider(tenantId, providerId2));
26342
27728
  }
26343
27729
  },
26344
27730
  log: logger6
@@ -26398,17 +27784,41 @@ var start = async (config) => {
26398
27784
  log: logger6
26399
27785
  });
26400
27786
  outboxRetention.start();
27787
+ let agentTaskConsumer;
26401
27788
  const queueServiceConfig = config?.queueServiceConfig;
26402
27789
  if (queueServiceConfig) {
26403
27790
  setQueueServiceType(queueServiceConfig.type);
26404
27791
  if (queueServiceConfig.defaultStartPollingQueue) {
26405
- const agentTaskConsumer = new AgentTaskConsumer(target_port);
27792
+ agentTaskConsumer = new AgentTaskConsumer(target_port);
26406
27793
  agentTaskConsumer.startPollingQueue();
26407
27794
  }
26408
27795
  }
26409
27796
  restoreMcpConnections().catch((error) => {
26410
27797
  logger6.error("MCP connection restoration failed", { error });
26411
27798
  });
27799
+ if (config?.gracefulShutdown !== false && !shutdownHandlersInstalled) {
27800
+ shutdownHandlersInstalled = true;
27801
+ const gracefulShutdown = createGracefulShutdown({
27802
+ timeoutMs: config?.gracefulShutdownTimeoutMs ?? 1e4,
27803
+ log: {
27804
+ info: (message, context) => logger6.info(message, context),
27805
+ warn: (message, context) => logger6.warn(message, context)
27806
+ },
27807
+ steps: [
27808
+ async () => {
27809
+ await Promise.all(
27810
+ agentInstanceManager9.getAllAgents().map((agent) => agent.abort().catch(() => void 0))
27811
+ );
27812
+ },
27813
+ () => agentTaskConsumer?.stopPollingQueue(),
27814
+ () => outboxPoller.stop(),
27815
+ () => outboxRetention.stop(),
27816
+ () => app.close()
27817
+ ]
27818
+ });
27819
+ process.once("SIGTERM", () => void gracefulShutdown("SIGTERM"));
27820
+ process.once("SIGINT", () => void gracefulShutdown("SIGINT"));
27821
+ }
26412
27822
  } catch (err) {
26413
27823
  logger6.error("Server start failed", { error: err });
26414
27824
  process.exit(1);
@@ -26422,13 +27832,22 @@ var LatticeGateway = {
26422
27832
  AgentTaskConsumer
26423
27833
  };
26424
27834
  export {
27835
+ A2AAuthService,
27836
+ A2A_KEY_CHANGED_TOPIC,
26425
27837
  LatticeGateway,
27838
+ MODEL_PROVIDER_CHANGED_TOPIC,
27839
+ ModelProviderService,
26426
27840
  ProjectRoomEventBroker,
26427
27841
  ROOM_EVENT_TOPIC,
26428
27842
  createOrphanRecoveryRuntime,
26429
27843
  createRecoveryCallerDeps,
26430
27844
  createTaskRunLiveness,
27845
+ handleA2AKeyChanged,
27846
+ handleModelProviderChanged,
26431
27847
  ingestRoomEvent,
27848
+ parseEnvKeys,
27849
+ publishA2AKeyChanged,
27850
+ publishModelProviderChanged,
26432
27851
  publishRoomEvent,
26433
27852
  recoverTask,
26434
27853
  recoverTopCaller,