@axiom-lattice/gateway 4.8.3 → 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 (39) 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-MVSXAG5S.js → embed-page-AgentDetailSideApp-BVJK7WIS.js} +1 -1
  6. package/dist/embed/{embed-page-AgentGovernanceWorkbench-ZEVAVRIQ.js → embed-page-AgentGovernanceWorkbench-L2YHPRNE.js} +1 -1
  7. package/dist/embed/{embed-page-MessageGraph-CPZJIKOD.js → embed-page-MessageGraph-JOQ46UE2.js} +1 -1
  8. package/dist/embed/{embed-page-ReactChart-7NQMTFJE.js → embed-page-ReactChart-I5AYTSII.js} +1 -1
  9. package/dist/embed/{embed-page-SkillFlow-TSBZ4V2P.js → embed-page-SkillFlow-YLB5XPFH.js} +1 -1
  10. package/dist/embed/{embed-page-TasksSideApp-CY6NI4DD.js → embed-page-TasksSideApp-QLJVB7UK.js} +1 -1
  11. package/dist/embed/{embed-page-TeamWorkspace-7GQCMJWF.js → embed-page-TeamWorkspace-BZZEAYSB.js} +1 -1
  12. package/dist/embed/{embed-page-TopologyRuntimeView-5DJPLCLQ.js → embed-page-TopologyRuntimeView-FRA3AXQQ.js} +1 -1
  13. package/dist/embed/{embed-page-WorkflowAutomationView-PDVXZXEF.js → embed-page-WorkflowAutomationView-D745PK3W.js} +1 -1
  14. package/dist/embed/{embed-page-chunk-KEL6XU6R.js → embed-page-chunk-2U4LKIE3.js} +1 -1
  15. package/dist/embed/{embed-page-chunk-AC6X2XRH.js → embed-page-chunk-3WRJEEZS.js} +1 -1
  16. package/dist/embed/{embed-page-chunk-3SV3E6X5.js → embed-page-chunk-4WBAN7QL.js} +209 -305
  17. package/dist/embed/{embed-page-chunk-Z7USDLHI.js → embed-page-chunk-6KCBXVIT.js} +1 -1
  18. package/dist/embed/embed-page-chunk-FVMSMU4J.js +242 -0
  19. package/dist/embed/{embed-page-chunk-SFHRRNYQ.js → embed-page-chunk-FWGNJMXD.js} +1 -1
  20. package/dist/embed/{embed-page-chunk-O4QK5LWZ.js → embed-page-chunk-FZRTVFHF.js} +83 -83
  21. package/dist/embed/{embed-page-chunk-UWL37DAK.js → embed-page-chunk-K5YYUAPL.js} +1 -1
  22. package/dist/embed/{embed-page-chunk-BKCIU75D.js → embed-page-chunk-M2PV36MX.js} +1 -1
  23. package/dist/embed/{embed-page-chunk-IV5C4DYL.js → embed-page-chunk-ONN5O5BX.js} +1 -1
  24. package/dist/embed/{embed-page-chunk-ULBEL4OA.js → embed-page-chunk-P5PFV3N7.js} +1 -1
  25. package/dist/embed/{embed-page-chunk-YPNSOZYW.js → embed-page-chunk-WMLR2GQK.js} +4 -4
  26. package/dist/embed/{embed-page-code-highlighter-Y6CQA4CN.js → embed-page-code-highlighter-NZBTSABL.js} +1 -1
  27. package/dist/embed/{embed-page-mermaid-CW4QCAH3.js → embed-page-mermaid-CK4ARQCM.js} +1 -1
  28. package/dist/embed/embed-page.js +1 -1
  29. package/dist/index.d.mts +383 -2
  30. package/dist/index.d.ts +383 -2
  31. package/dist/index.js +2563 -1094
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +1504 -82
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/{open-exposure-VU227IO3.mjs → open-exposure-27ESFWTB.mjs} +4 -4
  36. package/package.json +7 -7
  37. package/dist/a2a-FGYX2HH3.mjs.map +0 -1
  38. package/dist/embed/embed-page-chunk-PZCET7LJ.js +0 -245
  39. /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);
@@ -18673,7 +19000,11 @@ function registerCoSpaceRoutes(app2, deps) {
18673
19000
 
18674
19001
  // src/routes/push-notifications.ts
18675
19002
  import { ZodError as ZodError7, z as z11 } from "zod";
18676
- var subscriptionSchema = z11.object({ endpoint: z11.string().min(1), keys: z11.object({ p256dh: z11.string().min(1), auth: z11.string().min(1) }).strict() }).strict();
19003
+ var subscriptionSchema = z11.object({
19004
+ endpoint: z11.string().min(1),
19005
+ keys: z11.object({ p256dh: z11.string().min(1), auth: z11.string().min(1) }).strict(),
19006
+ expirationTime: z11.number().nullable().optional()
19007
+ }).strict();
18677
19008
  var deleteSchema = z11.object({ endpoint: z11.string().min(1) }).strict();
18678
19009
  function actor2(request) {
18679
19010
  const user = request.user;
@@ -18964,6 +19295,12 @@ var RoomDispatchService = class {
18964
19295
  if (!source) throw new Error("Invalid project room dispatch message");
18965
19296
  const anchorMessageId = source.sourceId ?? source.id;
18966
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
+ }
18967
19304
  const results = [];
18968
19305
  for (const entry of entries) {
18969
19306
  if (entry.kind === "failure") {
@@ -19003,7 +19340,8 @@ var RoomDispatchService = class {
19003
19340
  role: membership.role,
19004
19341
  title: membership.title,
19005
19342
  source: "project_room",
19006
- ...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility }
19343
+ ...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility },
19344
+ ...rosterExempt ? { rosterExempt: true } : {}
19007
19345
  } }
19008
19346
  }
19009
19347
  });
@@ -21533,7 +21871,6 @@ function isSubscriptionGone(error) {
21533
21871
  const statusCode = error.statusCode;
21534
21872
  return statusCode === 404 || statusCode === 410;
21535
21873
  }
21536
- var MAX_BODY = 140;
21537
21874
  var RoomPushNotifier = class {
21538
21875
  /** Creates a notifier from explicitly injected collaborators. */
21539
21876
  constructor(deps) {
@@ -21557,7 +21894,7 @@ var RoomPushNotifier = class {
21557
21894
  const title = await this.deps.resolveTitle?.(message) ?? message.roomId;
21558
21895
  const payload = {
21559
21896
  title,
21560
- body: message.content.text.slice(0, MAX_BODY),
21897
+ body: "notification",
21561
21898
  tag: message.projectId,
21562
21899
  url: this.deps.buildUrl?.(message) ?? "/"
21563
21900
  };
@@ -22367,6 +22704,14 @@ async function transferGovernedDispatchOwner(task, options, deps) {
22367
22704
  return result.task;
22368
22705
  });
22369
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
+ }
22370
22715
  var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22371
22716
  const webAppStore = getStoreLattice17("default", "agentWebApp").store;
22372
22717
  const assistantStore = getStoreLattice17("default", "assistant").store;
@@ -22482,7 +22827,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22482
22827
  });
22483
22828
  return String(await agent.getRunStatus());
22484
22829
  },
22485
- dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope))
22830
+ dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope)),
22831
+ projects: projectStore
22486
22832
  });
22487
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 });
22488
22834
  const rosterService = new ProjectRosterService({
@@ -22490,7 +22836,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22490
22836
  roomService,
22491
22837
  membershipStore: projectBotMembershipStore,
22492
22838
  projects: projectStore,
22493
- 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 } })) },
22494
22840
  threadStore,
22495
22841
  assistantStore,
22496
22842
  taskStore,
@@ -22504,7 +22850,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22504
22850
  resolver: new RoomTargetResolver({ botStore: projectBotMembershipStore }),
22505
22851
  installation,
22506
22852
  bindings: bindingLifecycle,
22507
- router: channelDeps.router
22853
+ router: channelDeps.router,
22854
+ projects: projectStore
22508
22855
  }) : new UnavailableRoomDispatcher();
22509
22856
  const coordinatorWakeNotifier = dispatcher instanceof RoomDispatchService ? new CoordinatorWakeNotifier({
22510
22857
  tasks: taskStore,
@@ -22524,6 +22871,12 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22524
22871
  botStore: projectBotMembershipStore,
22525
22872
  messageStore: projectRoomMessageStore,
22526
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
+ },
22527
22880
  realtime,
22528
22881
  onRealtimeFailure: realtime && channelDeps?.warn ? (event) => channelDeps.warn?.("project-room:message-realtime-callback-failed", {
22529
22882
  code: event.code,
@@ -22540,6 +22893,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22540
22893
  threadStore,
22541
22894
  realtime,
22542
22895
  log: options.projectTaskLog,
22896
+ ensurePublicOwner: (scope) => rosterService.ensurePublicProjectAgent(scope),
22543
22897
  ...coordinatorWakeNotifier === void 0 ? {} : { coordinatorWake: coordinatorWakeNotifier }
22544
22898
  }) : { reason: void 0 };
22545
22899
  const projectTaskComposition = "composition" in projectTaskRuntime ? projectTaskRuntime.composition : void 0;
@@ -22667,7 +23021,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
22667
23021
  has: (tenantId, modelKey2) => modelLatticeManager3.hasLatticeWithTenant(tenantId, modelKey2),
22668
23022
  remove: (tenantId, modelKey2) => modelLatticeManager3.removeLatticeWithTenant(tenantId, modelKey2),
22669
23023
  listRegistered: (tenantId) => modelLatticeManager3.getLatticeKeysByTenant(tenantId)
22670
- }
23024
+ },
23025
+ onChanged: (tenantId, providerId2) => broadcastModelProviderChanged(tenantId, providerId2)
22671
23026
  }, { authRequired: process.env.AUTH_REQUIRED === "true" });
22672
23027
  registerModelProviderRoutes(app2, modelProviderController);
22673
23028
  void modelProviderController.hydrateRegistry().catch(() => void 0);
@@ -23126,7 +23481,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
23126
23481
  return {
23127
23482
  ...projectTaskComposition ? { projectTaskRuntime: projectTaskComposition.lease } : {},
23128
23483
  ...projectTaskUnavailableReason ? { projectTaskUnavailableReason } : {},
23129
- ...projectRoomRealtimeRuntime ? { projectRoomRealtimeRuntime } : {}
23484
+ ...projectRoomRealtimeRuntime ? { projectRoomRealtimeRuntime } : {},
23485
+ modelProviderController
23130
23486
  };
23131
23487
  };
23132
23488
  function tryCreateProjectTaskRuntime(input) {
@@ -23197,20 +23553,24 @@ function createRuntime(input, stores, log2) {
23197
23553
  workspace_id: task.workspaceId,
23198
23554
  project_id: task.projectId
23199
23555
  });
23200
- const execution = new ProjectTaskExecutionService({
23201
- tasks: input.taskStore,
23202
- projects: input.projectStore,
23203
- rooms: input.projectRoomStore,
23204
- bots: input.projectBotMembershipStore,
23205
- threads,
23206
- lifecycle,
23207
- dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope)),
23208
- queue: stores.queue,
23209
- agentFor
23210
- }, {
23211
- settlementTimeoutMs: PROJECT_TASK_SETTLEMENT_TIMEOUT_MS,
23212
- lifecycleSettlementTimeoutMs: PROJECT_TASK_LIFECYCLE_SETTLEMENT_TIMEOUT_MS
23213
- });
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
+ );
23214
23574
  const observer = new ProjectTaskObserver({
23215
23575
  tasks: input.taskStore,
23216
23576
  workItems: input.taskWorkItemStore,
@@ -25088,6 +25448,40 @@ var _AgentTaskConsumer = class _AgentTaskConsumer {
25088
25448
  _AgentTaskConsumer.agent_run_endpoint = "http://localhost:4001/api/runs";
25089
25449
  var AgentTaskConsumer = _AgentTaskConsumer;
25090
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
+
25091
25485
  // src/index.ts
25092
25486
  import {
25093
25487
  registerLoggerLattice,
@@ -25103,7 +25497,7 @@ import {
25103
25497
  createTrustedAgentMessageDispatcher as createTrustedAgentMessageDispatcher2,
25104
25498
  runAsLeader,
25105
25499
  configureCoordinationLeaseStore,
25106
- getReplicaId as getReplicaId6,
25500
+ getReplicaId as getReplicaId7,
25107
25501
  taskLifecycleNotifier as taskLifecycleNotifier3,
25108
25502
  projectTaskAdmissionNotifier as projectTaskAdmissionNotifier2,
25109
25503
  scheduleLatticeManager as scheduleLatticeManager2,
@@ -25112,7 +25506,9 @@ import {
25112
25506
  agentLatticeManager as agentLatticeManager5,
25113
25507
  configurePluginCatalogSync,
25114
25508
  PLUGIN_CATALOG_CHANGED_TOPIC,
25115
- getSandboxPluginCatalog as getSandboxPluginCatalog3
25509
+ getSandboxPluginCatalog as getSandboxPluginCatalog3,
25510
+ getSandBoxManager as getSandBoxManager4,
25511
+ installPluginHost
25116
25512
  } from "@axiom-lattice/core";
25117
25513
 
25118
25514
  // src/channels/ChannelInfrastructure.ts
@@ -25220,7 +25616,7 @@ function validateRoomTrustedDispatch(message, options, binding, agentId) {
25220
25616
  "inputMessageId",
25221
25617
  "role",
25222
25618
  "title"
25223
- ], ["responsibility", "source"]);
25619
+ ], ["responsibility", "source", "rosterExempt"]);
25224
25620
  if (!binding || !targetEnvelope || !target || !context || !authority || typeof authority.expectedRoomThreadId !== "string" || authority.expectedRoomThreadId.length === 0 || trusted?.executionMode !== "followup") return false;
25225
25621
  const expectedSender = `room:${context.roomId}:membership:${context.membershipId}`;
25226
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;
@@ -25781,9 +26177,942 @@ async function prepareProjectTaskRuntime(runtime, log2) {
25781
26177
  }
25782
26178
  }
25783
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
+
25784
27106
  // src/services/multiReplicaReadiness.ts
25785
27107
  import { getStoreLattice as getStoreLattice28 } from "@axiom-lattice/core";
25786
- 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
+ ];
25787
27116
  function storeClassName(kind) {
25788
27117
  try {
25789
27118
  return getStoreLattice28("default", kind).store.constructor?.name;
@@ -26055,6 +27384,7 @@ app.setErrorHandler((error, request, reply) => {
26055
27384
  error: error.message || "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF"
26056
27385
  });
26057
27386
  });
27387
+ var shutdownHandlersInstalled = false;
26058
27388
  function getConfiguredSandboxProvider() {
26059
27389
  const sandboxProviderType = process.env.SANDBOX_PROVIDER_TYPE || "microsandbox-remote";
26060
27390
  return createSandboxProvider({
@@ -26188,18 +27518,18 @@ var start = async (config) => {
26188
27518
  projectRoomRealtimeRuntime
26189
27519
  });
26190
27520
  try {
26191
- const { A2AAuthService, parseEnvKeys } = await import("./A2AAuthService-7UGQ7GMW.mjs");
27521
+ const { A2AAuthService: A2AAuthService2, parseEnvKeys: parseEnvKeys2 } = await import("./A2AAuthService-7UGQ7GMW.mjs");
26192
27522
  const { registerA2AStandardRoutes } = await import("./a2a-standard-PBYPNHOA.mjs");
26193
- const a2a = await import("./a2a-FGYX2HH3.mjs");
27523
+ const a2a = await import("./a2a-BHBLACJV.mjs");
26194
27524
  const a2aKeyStore = getStoreLattice29("default", "a2aApiKey").store;
26195
27525
  const assistantStore = getStoreLattice29("default", "assistant").store;
26196
27526
  const projectStore = getStoreLattice29("default", "project").store;
26197
27527
  const taskStore = getStoreLattice29("default", "task").store;
26198
- const authService = new A2AAuthService({
27528
+ const authService = new A2AAuthService2({
26199
27529
  keyStore: a2aKeyStore,
26200
27530
  assistantStore,
26201
27531
  projectStore,
26202
- envKeyMap: parseEnvKeys(process.env.A2A_API_KEYS || ""),
27532
+ envKeyMap: parseEnvKeys2(process.env.A2A_API_KEYS || ""),
26203
27533
  allowUnauthenticated: process.env.A2A_ALLOW_UNAUTHENTICATED === "true"
26204
27534
  });
26205
27535
  await authService.refresh();
@@ -26224,7 +27554,7 @@ var start = async (config) => {
26224
27554
  const { registerOpenCatalogRoutes } = await import("./open-catalog-SZQ435AY.mjs");
26225
27555
  const { registerOpenFilesRoutes } = await import("./open-files-X3HXVEEM.mjs");
26226
27556
  const { registerOpenAuditRoutes } = await import("./open-audit-UJ3NXCGW.mjs");
26227
- const { registerOpenExposureRoutes } = await import("./open-exposure-VU227IO3.mjs");
27557
+ const { registerOpenExposureRoutes } = await import("./open-exposure-27ESFWTB.mjs");
26228
27558
  const { openLattice } = await import("./openLattice-Q6M5S6CY.mjs");
26229
27559
  const { createKbProvider } = await import("./kbProvider-2D6DZ34N.mjs");
26230
27560
  const { createOpenSandboxProvider } = await import("./sandboxProvider-DUYWHYJN.mjs");
@@ -26272,6 +27602,50 @@ var start = async (config) => {
26272
27602
  sandboxLatticeManager2.registerLattice("default", getConfiguredSandboxProvider());
26273
27603
  logger6.info("Registered sandbox manager from env configuration");
26274
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
+ }
26275
27649
  try {
26276
27650
  const { ResourceController } = await import("./resources-2L3KSUUU.mjs");
26277
27651
  const sharedResourceStore = getStoreLattice29("default", "sharedResource").store;
@@ -26290,12 +27664,21 @@ var start = async (config) => {
26290
27664
  });
26291
27665
  }
26292
27666
  if (channelDeps && process.env.CHANNELS_ENABLED !== "false") {
26293
- const { connectAllChannels } = await import("@axiom-lattice/core");
27667
+ const { connectAllChannels, disconnectAllChannels, ChannelConnectionLeader } = await import("@axiom-lattice/core");
26294
27668
  try {
26295
- await connectAllChannels(
26296
- (channel) => adapterRegistry.get(channel),
26297
- { deps: { router: channelDeps.router } }
26298
- );
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();
26299
27682
  } catch (err) {
26300
27683
  logger6.error("Failed to start channel connections", {
26301
27684
  error: err instanceof Error ? err.message : String(err)
@@ -26307,20 +27690,20 @@ var start = async (config) => {
26307
27690
  const outboxStore = getStoreLattice29("default", "outbox").store;
26308
27691
  taskLifecycleNotifier3.setOutbox(outboxStore);
26309
27692
  projectTaskAdmissionNotifier2.setOutbox(outboxStore);
26310
- configurePluginCatalogSync({ outbox: outboxStore, owner: getReplicaId6() });
27693
+ configurePluginCatalogSync({ outbox: outboxStore, owner: getReplicaId7() });
26311
27694
  bridgeAssistantEvents(
26312
27695
  (event, cb) => eventBus4.subscribe(event, cb),
26313
27696
  outboxStore,
26314
- getReplicaId6()
27697
+ getReplicaId7()
26315
27698
  );
26316
27699
  if (projectRoomRealtimeRuntime) {
26317
27700
  projectRoomRealtimeRuntime.broker.setBroadcastHook((scope, serialized, sourceKey) => {
26318
- void publishRoomEvent(outboxStore, getReplicaId6(), scope, serialized, sourceKey);
27701
+ void publishRoomEvent(outboxStore, getReplicaId7(), scope, serialized, sourceKey);
26319
27702
  });
26320
27703
  }
26321
27704
  const outboxPoller = new OutboxPoller({
26322
27705
  store: outboxStore,
26323
- owner: getReplicaId6(),
27706
+ owner: getReplicaId7(),
26324
27707
  handlers: {
26325
27708
  "task-lifecycle": (payload) => taskLifecycleNotifier3.deliverFromOutbox(payload),
26326
27709
  "project-task-admission": (payload) => projectTaskAdmissionNotifier2.deliverFromOutbox(payload),
@@ -26328,6 +27711,7 @@ var start = async (config) => {
26328
27711
  if (projectRoomRealtimeRuntime) ingestRoomEvent(payload, projectRoomRealtimeRuntime.broker);
26329
27712
  },
26330
27713
  "assistant-changed": (payload) => handleAssistantChanged(payload, (tenantId) => agentLatticeManager5.initializeStoredAssistantsForTenant(tenantId)),
27714
+ [A2A_KEY_CHANGED_TOPIC]: (payload) => handleA2AKeyChanged(payload, () => refreshStoreKeyMap()),
26331
27715
  [MCP_CONFIG_CHANGED_TOPIC]: (payload) => reconcileMcpConfigChange(payload, {
26332
27716
  store: getStoreLattice29("default", "mcp").store,
26333
27717
  manager: mcpManager3,
@@ -26336,6 +27720,11 @@ var start = async (config) => {
26336
27720
  [PLUGIN_CATALOG_CHANGED_TOPIC]: async (payload) => {
26337
27721
  const tenantId = payload?.tenantId;
26338
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));
26339
27728
  }
26340
27729
  },
26341
27730
  log: logger6
@@ -26395,17 +27784,41 @@ var start = async (config) => {
26395
27784
  log: logger6
26396
27785
  });
26397
27786
  outboxRetention.start();
27787
+ let agentTaskConsumer;
26398
27788
  const queueServiceConfig = config?.queueServiceConfig;
26399
27789
  if (queueServiceConfig) {
26400
27790
  setQueueServiceType(queueServiceConfig.type);
26401
27791
  if (queueServiceConfig.defaultStartPollingQueue) {
26402
- const agentTaskConsumer = new AgentTaskConsumer(target_port);
27792
+ agentTaskConsumer = new AgentTaskConsumer(target_port);
26403
27793
  agentTaskConsumer.startPollingQueue();
26404
27794
  }
26405
27795
  }
26406
27796
  restoreMcpConnections().catch((error) => {
26407
27797
  logger6.error("MCP connection restoration failed", { error });
26408
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
+ }
26409
27822
  } catch (err) {
26410
27823
  logger6.error("Server start failed", { error: err });
26411
27824
  process.exit(1);
@@ -26419,13 +27832,22 @@ var LatticeGateway = {
26419
27832
  AgentTaskConsumer
26420
27833
  };
26421
27834
  export {
27835
+ A2AAuthService,
27836
+ A2A_KEY_CHANGED_TOPIC,
26422
27837
  LatticeGateway,
27838
+ MODEL_PROVIDER_CHANGED_TOPIC,
27839
+ ModelProviderService,
26423
27840
  ProjectRoomEventBroker,
26424
27841
  ROOM_EVENT_TOPIC,
26425
27842
  createOrphanRecoveryRuntime,
26426
27843
  createRecoveryCallerDeps,
26427
27844
  createTaskRunLiveness,
27845
+ handleA2AKeyChanged,
27846
+ handleModelProviderChanged,
26428
27847
  ingestRoomEvent,
27848
+ parseEnvKeys,
27849
+ publishA2AKeyChanged,
27850
+ publishModelProviderChanged,
26429
27851
  publishRoomEvent,
26430
27852
  recoverTask,
26431
27853
  recoverTopCaller,