@axiom-lattice/gateway 4.8.4 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a2a-BHBLACJV.mjs +17 -0
- package/dist/a2a-BHBLACJV.mjs.map +1 -0
- package/dist/{a2a-FGYX2HH3.mjs → chunk-HYYLYIMH.mjs} +59 -15
- package/dist/chunk-HYYLYIMH.mjs.map +1 -0
- package/dist/embed/{embed-page-AgentDetailSideApp-S5N76UPU.js → embed-page-AgentDetailSideApp-BVJK7WIS.js} +1 -1
- package/dist/embed/{embed-page-AgentGovernanceWorkbench-QFMP2M73.js → embed-page-AgentGovernanceWorkbench-L2YHPRNE.js} +1 -1
- package/dist/embed/{embed-page-MessageGraph-H3HPZKOH.js → embed-page-MessageGraph-JOQ46UE2.js} +1 -1
- package/dist/embed/{embed-page-ReactChart-ZFMANJCT.js → embed-page-ReactChart-I5AYTSII.js} +1 -1
- package/dist/embed/{embed-page-SkillFlow-HY7PD43P.js → embed-page-SkillFlow-YLB5XPFH.js} +1 -1
- package/dist/embed/{embed-page-TasksSideApp-QKEXMHCT.js → embed-page-TasksSideApp-QLJVB7UK.js} +1 -1
- package/dist/embed/{embed-page-TeamWorkspace-ZFVAFZXZ.js → embed-page-TeamWorkspace-BZZEAYSB.js} +1 -1
- package/dist/embed/{embed-page-TopologyRuntimeView-2BS4OYEZ.js → embed-page-TopologyRuntimeView-FRA3AXQQ.js} +1 -1
- package/dist/embed/{embed-page-WorkflowAutomationView-WUI7BSQP.js → embed-page-WorkflowAutomationView-D745PK3W.js} +1 -1
- package/dist/embed/{embed-page-chunk-5WYDCL3B.js → embed-page-chunk-4WBAN7QL.js} +7 -7
- package/dist/embed/{embed-page-chunk-4T4QMOUS.js → embed-page-chunk-FVMSMU4J.js} +1 -1
- package/dist/embed/{embed-page-chunk-VJTO6SG6.js → embed-page-chunk-FWGNJMXD.js} +1 -1
- package/dist/embed/{embed-page-chunk-MSHEVUK3.js → embed-page-chunk-K5YYUAPL.js} +1 -1
- package/dist/embed/{embed-page-chunk-6HM7ZERK.js → embed-page-chunk-M2PV36MX.js} +1 -1
- package/dist/embed/{embed-page-chunk-XKQ6YPUW.js → embed-page-chunk-P5PFV3N7.js} +1 -1
- package/dist/embed/{embed-page-chunk-7ZBZKXBU.js → embed-page-chunk-WMLR2GQK.js} +4 -4
- package/dist/embed/embed-page.js +1 -1
- package/dist/index.d.mts +383 -2
- package/dist/index.d.ts +383 -2
- package/dist/index.js +2557 -1091
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1498 -79
- package/dist/index.mjs.map +1 -1
- package/dist/{open-exposure-VU227IO3.mjs → open-exposure-27ESFWTB.mjs} +4 -4
- package/package.json +7 -7
- package/dist/a2a-FGYX2HH3.mjs.map +0 -1
- /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,
|
|
5126
|
-
const encodedError = encodeURIComponent(
|
|
5127
|
-
return ERROR_HTML.replace("{assistantId}", assistantId).replace("{threadId}", threadId).replace("{vmIsolation}", vmIsolation).replace("{errorMessage}",
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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 —
|
|
17218
|
-
*
|
|
17219
|
-
*
|
|
17220
|
-
*
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
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" },
|
|
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
|
-
|
|
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,
|
|
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 &&
|
|
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
|
-
|
|
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
|
|
17456
|
-
|
|
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
|
|
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,
|
|
17872
|
+
async persistNormalizedHumanMessage(actor3, input, room) {
|
|
17873
|
+
const snapshot = await this.resolvePublicMentions(actor3, input);
|
|
17592
17874
|
for (const membershipId of snapshot.botIds) await this.validateMentionIdentity(actor3, room, membershipId);
|
|
17593
17875
|
let reply = null;
|
|
17594
17876
|
if (snapshot.replyToMessageId !== void 0) {
|
|
@@ -17615,6 +17897,38 @@ var RoomMessageService = class {
|
|
|
17615
17897
|
}
|
|
17616
17898
|
return { message: validated, replayed };
|
|
17617
17899
|
}
|
|
17900
|
+
/**
|
|
17901
|
+
* Rewrites public-project virtual mention ids to their real membership ids,
|
|
17902
|
+
* materializing each mentioned agent just-in-time.
|
|
17903
|
+
*
|
|
17904
|
+
* @param actor Authenticated sender.
|
|
17905
|
+
* @param snapshot Normalized message with possibly-virtual bot mention ids.
|
|
17906
|
+
* @returns The snapshot unchanged, or one whose bot mentions carry real ids.
|
|
17907
|
+
*/
|
|
17908
|
+
async resolvePublicMentions(actor3, snapshot) {
|
|
17909
|
+
const resolver = this.deps.resolveMentionTarget;
|
|
17910
|
+
if (!resolver || snapshot.botIds.length === 0) return snapshot;
|
|
17911
|
+
let changed = false;
|
|
17912
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17913
|
+
const mentions = [];
|
|
17914
|
+
for (const mention of snapshot.mentions) {
|
|
17915
|
+
if (mention.type !== "bot") {
|
|
17916
|
+
mentions.push(mention);
|
|
17917
|
+
continue;
|
|
17918
|
+
}
|
|
17919
|
+
const real = await resolver(actor3, actor3.projectId, mention.membershipId);
|
|
17920
|
+
if (real !== mention.membershipId) changed = true;
|
|
17921
|
+
if (seen.has(real)) {
|
|
17922
|
+
changed = true;
|
|
17923
|
+
continue;
|
|
17924
|
+
}
|
|
17925
|
+
seen.add(real);
|
|
17926
|
+
mentions.push({ type: "bot", membershipId: real });
|
|
17927
|
+
}
|
|
17928
|
+
if (!changed) return snapshot;
|
|
17929
|
+
const botIds = mentions.filter((mention) => mention.type === "bot").map((mention) => mention.membershipId);
|
|
17930
|
+
return { ...snapshot, mentions, botIds };
|
|
17931
|
+
}
|
|
17618
17932
|
/**
|
|
17619
17933
|
* Persists a human message, then resolves and dispatches its current targets.
|
|
17620
17934
|
* Idempotent outcomes are single-flight and retained only in this service process; a
|
|
@@ -17853,6 +18167,12 @@ var RoomInterruptService = class {
|
|
|
17853
18167
|
throw new RoomInterruptServiceError("ROOM_INTERRUPT_NOT_PENDING", 409, "Room interrupt is no longer pending");
|
|
17854
18168
|
}
|
|
17855
18169
|
const inputMessageId = this.idFactory();
|
|
18170
|
+
let rosterExempt = false;
|
|
18171
|
+
try {
|
|
18172
|
+
rosterExempt = isRosterExemptProject(await this.deps.projects.getProjectById(membership.tenantId, membership.projectId));
|
|
18173
|
+
} catch {
|
|
18174
|
+
rosterExempt = false;
|
|
18175
|
+
}
|
|
17856
18176
|
await this.deps.dispatcher.dispatch(
|
|
17857
18177
|
{
|
|
17858
18178
|
tenant_id: scope.tenantId,
|
|
@@ -17877,7 +18197,8 @@ var RoomInterruptService = class {
|
|
|
17877
18197
|
role: membership.role,
|
|
17878
18198
|
title: membership.title,
|
|
17879
18199
|
source: "project_room",
|
|
17880
|
-
...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility }
|
|
18200
|
+
...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility },
|
|
18201
|
+
...rosterExempt ? { rosterExempt: true } : {}
|
|
17881
18202
|
}
|
|
17882
18203
|
}
|
|
17883
18204
|
}
|
|
@@ -17902,7 +18223,7 @@ var createRoomMessageSchema = z10.object({
|
|
|
17902
18223
|
mentions: z10.array(z10.union([
|
|
17903
18224
|
z10.object({ type: z10.literal("team") }).strict(),
|
|
17904
18225
|
z10.object({ type: z10.literal("bot"), membershipId: id2 }).strict()
|
|
17905
|
-
])).optional(),
|
|
18226
|
+
])).max(50).optional(),
|
|
17906
18227
|
replyToMessageId: id2.optional()
|
|
17907
18228
|
}).strict();
|
|
17908
18229
|
var addProjectMemberSchema = z10.object({ userId: id2, role: z10.enum(["owner", "admin", "member", "viewer"]) }).strict();
|
|
@@ -18061,6 +18382,12 @@ function registerProjectRosterRoutes(app2, deps) {
|
|
|
18061
18382
|
const body = mutationExpectedUpdatedAtSchema.parse(request.body);
|
|
18062
18383
|
return { data: await method.call(deps.service, getActor(request, p.projectId), { membershipId: p.membershipId, expectedUpdatedAt: body.expectedUpdatedAt }) };
|
|
18063
18384
|
}));
|
|
18385
|
+
app2.post("/api/projects/:projectId/bots/:membershipId/ensure", async (request, reply) => execute(reply, async () => {
|
|
18386
|
+
const p = membershipParamsSchema.parse(request.params);
|
|
18387
|
+
emptyObjectSchema.parse(request.body ?? {});
|
|
18388
|
+
emptyObjectSchema.parse(request.query ?? {});
|
|
18389
|
+
return { data: await deps.service.ensureBotMembership(getActor(request, p.projectId), p.membershipId) };
|
|
18390
|
+
}));
|
|
18064
18391
|
app2.delete("/api/projects/:projectId/bots/:membershipId", async (request, reply) => execute(reply, async () => {
|
|
18065
18392
|
const p = membershipParamsSchema.parse(request.params);
|
|
18066
18393
|
const q = mutationExpectedUpdatedAtSchema.parse(request.query);
|
|
@@ -18968,6 +19295,12 @@ var RoomDispatchService = class {
|
|
|
18968
19295
|
if (!source) throw new Error("Invalid project room dispatch message");
|
|
18969
19296
|
const anchorMessageId = source.sourceId ?? source.id;
|
|
18970
19297
|
const entries = await this.deps.resolver.resolve(source);
|
|
19298
|
+
let rosterExempt = false;
|
|
19299
|
+
try {
|
|
19300
|
+
rosterExempt = isRosterExemptProject(await this.deps.projects.getProjectById(source.tenantId, source.projectId));
|
|
19301
|
+
} catch {
|
|
19302
|
+
rosterExempt = false;
|
|
19303
|
+
}
|
|
18971
19304
|
const results = [];
|
|
18972
19305
|
for (const entry of entries) {
|
|
18973
19306
|
if (entry.kind === "failure") {
|
|
@@ -19007,7 +19340,8 @@ var RoomDispatchService = class {
|
|
|
19007
19340
|
role: membership.role,
|
|
19008
19341
|
title: membership.title,
|
|
19009
19342
|
source: "project_room",
|
|
19010
|
-
...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility }
|
|
19343
|
+
...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility },
|
|
19344
|
+
...rosterExempt ? { rosterExempt: true } : {}
|
|
19011
19345
|
} }
|
|
19012
19346
|
}
|
|
19013
19347
|
});
|
|
@@ -22370,6 +22704,14 @@ async function transferGovernedDispatchOwner(task, options, deps) {
|
|
|
22370
22704
|
return result.task;
|
|
22371
22705
|
});
|
|
22372
22706
|
}
|
|
22707
|
+
function broadcastModelProviderChanged(tenantId, providerId2) {
|
|
22708
|
+
if (!tenantId) return;
|
|
22709
|
+
try {
|
|
22710
|
+
const outbox = getStoreLattice17("default", "outbox").store;
|
|
22711
|
+
void publishModelProviderChanged(outbox, getReplicaId6(), tenantId, providerId2);
|
|
22712
|
+
} catch {
|
|
22713
|
+
}
|
|
22714
|
+
}
|
|
22373
22715
|
var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
22374
22716
|
const webAppStore = getStoreLattice17("default", "agentWebApp").store;
|
|
22375
22717
|
const assistantStore = getStoreLattice17("default", "assistant").store;
|
|
@@ -22485,7 +22827,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22485
22827
|
});
|
|
22486
22828
|
return String(await agent.getRunStatus());
|
|
22487
22829
|
},
|
|
22488
|
-
dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope))
|
|
22830
|
+
dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope)),
|
|
22831
|
+
projects: projectStore
|
|
22489
22832
|
});
|
|
22490
22833
|
const membershipService = new ProjectMembershipService({ membershipStore: projectMembershipStore, userTenantLinkStore, accessService, projectMutationMutex: projectMutationMutex2, roomStore: projectRoomStore, realtime, onRealtimeFailure: realtime && channelDeps?.warn ? ({ code, membershipId }) => channelDeps.warn?.("project-room:membership-realtime-failed", { code, membershipId }) : void 0 });
|
|
22491
22834
|
const rosterService = new ProjectRosterService({
|
|
@@ -22493,7 +22836,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22493
22836
|
roomService,
|
|
22494
22837
|
membershipStore: projectBotMembershipStore,
|
|
22495
22838
|
projects: projectStore,
|
|
22496
|
-
agents: { list: async (tenantId, userId) => (await collectTenantAssistants(tenantId, userId)).map((assistant) => ({ assistantId: assistant.id, name: assistant.name })) },
|
|
22839
|
+
agents: { list: async (tenantId, userId) => (await collectTenantAssistants(tenantId, userId)).map((assistant) => ({ assistantId: assistant.id, name: assistant.name, ...assistant.ownerUserId === void 0 ? {} : { ownerUserId: assistant.ownerUserId } })) },
|
|
22497
22840
|
threadStore,
|
|
22498
22841
|
assistantStore,
|
|
22499
22842
|
taskStore,
|
|
@@ -22507,7 +22850,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22507
22850
|
resolver: new RoomTargetResolver({ botStore: projectBotMembershipStore }),
|
|
22508
22851
|
installation,
|
|
22509
22852
|
bindings: bindingLifecycle,
|
|
22510
|
-
router: channelDeps.router
|
|
22853
|
+
router: channelDeps.router,
|
|
22854
|
+
projects: projectStore
|
|
22511
22855
|
}) : new UnavailableRoomDispatcher();
|
|
22512
22856
|
const coordinatorWakeNotifier = dispatcher instanceof RoomDispatchService ? new CoordinatorWakeNotifier({
|
|
22513
22857
|
tasks: taskStore,
|
|
@@ -22527,6 +22871,12 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22527
22871
|
botStore: projectBotMembershipStore,
|
|
22528
22872
|
messageStore: projectRoomMessageStore,
|
|
22529
22873
|
dispatcher,
|
|
22874
|
+
resolveMentionTarget: async (actor3, projectId, membershipId) => {
|
|
22875
|
+
const assistantId = publicProjectBotAssistantId(projectId, membershipId);
|
|
22876
|
+
if (assistantId === void 0) return membershipId;
|
|
22877
|
+
const membership = await rosterService.ensurePublicProjectBot({ tenantId: actor3.tenantId, userId: actor3.userId, projectId }, assistantId);
|
|
22878
|
+
return membership.id;
|
|
22879
|
+
},
|
|
22530
22880
|
realtime,
|
|
22531
22881
|
onRealtimeFailure: realtime && channelDeps?.warn ? (event) => channelDeps.warn?.("project-room:message-realtime-callback-failed", {
|
|
22532
22882
|
code: event.code,
|
|
@@ -22543,6 +22893,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22543
22893
|
threadStore,
|
|
22544
22894
|
realtime,
|
|
22545
22895
|
log: options.projectTaskLog,
|
|
22896
|
+
ensurePublicOwner: (scope) => rosterService.ensurePublicProjectAgent(scope),
|
|
22546
22897
|
...coordinatorWakeNotifier === void 0 ? {} : { coordinatorWake: coordinatorWakeNotifier }
|
|
22547
22898
|
}) : { reason: void 0 };
|
|
22548
22899
|
const projectTaskComposition = "composition" in projectTaskRuntime ? projectTaskRuntime.composition : void 0;
|
|
@@ -22670,7 +23021,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22670
23021
|
has: (tenantId, modelKey2) => modelLatticeManager3.hasLatticeWithTenant(tenantId, modelKey2),
|
|
22671
23022
|
remove: (tenantId, modelKey2) => modelLatticeManager3.removeLatticeWithTenant(tenantId, modelKey2),
|
|
22672
23023
|
listRegistered: (tenantId) => modelLatticeManager3.getLatticeKeysByTenant(tenantId)
|
|
22673
|
-
}
|
|
23024
|
+
},
|
|
23025
|
+
onChanged: (tenantId, providerId2) => broadcastModelProviderChanged(tenantId, providerId2)
|
|
22674
23026
|
}, { authRequired: process.env.AUTH_REQUIRED === "true" });
|
|
22675
23027
|
registerModelProviderRoutes(app2, modelProviderController);
|
|
22676
23028
|
void modelProviderController.hydrateRegistry().catch(() => void 0);
|
|
@@ -23129,7 +23481,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
23129
23481
|
return {
|
|
23130
23482
|
...projectTaskComposition ? { projectTaskRuntime: projectTaskComposition.lease } : {},
|
|
23131
23483
|
...projectTaskUnavailableReason ? { projectTaskUnavailableReason } : {},
|
|
23132
|
-
...projectRoomRealtimeRuntime ? { projectRoomRealtimeRuntime } : {}
|
|
23484
|
+
...projectRoomRealtimeRuntime ? { projectRoomRealtimeRuntime } : {},
|
|
23485
|
+
modelProviderController
|
|
23133
23486
|
};
|
|
23134
23487
|
};
|
|
23135
23488
|
function tryCreateProjectTaskRuntime(input) {
|
|
@@ -23200,20 +23553,24 @@ function createRuntime(input, stores, log2) {
|
|
|
23200
23553
|
workspace_id: task.workspaceId,
|
|
23201
23554
|
project_id: task.projectId
|
|
23202
23555
|
});
|
|
23203
|
-
const execution = new ProjectTaskExecutionService(
|
|
23204
|
-
|
|
23205
|
-
|
|
23206
|
-
|
|
23207
|
-
|
|
23208
|
-
|
|
23209
|
-
|
|
23210
|
-
|
|
23211
|
-
|
|
23212
|
-
|
|
23213
|
-
|
|
23214
|
-
|
|
23215
|
-
|
|
23216
|
-
|
|
23556
|
+
const execution = new ProjectTaskExecutionService(
|
|
23557
|
+
{
|
|
23558
|
+
tasks: input.taskStore,
|
|
23559
|
+
projects: input.projectStore,
|
|
23560
|
+
rooms: input.projectRoomStore,
|
|
23561
|
+
bots: input.projectBotMembershipStore,
|
|
23562
|
+
threads,
|
|
23563
|
+
lifecycle,
|
|
23564
|
+
dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope)),
|
|
23565
|
+
queue: stores.queue,
|
|
23566
|
+
agentFor,
|
|
23567
|
+
...input.ensurePublicOwner === void 0 ? {} : { ensurePublicOwner: input.ensurePublicOwner }
|
|
23568
|
+
},
|
|
23569
|
+
{
|
|
23570
|
+
settlementTimeoutMs: PROJECT_TASK_SETTLEMENT_TIMEOUT_MS,
|
|
23571
|
+
lifecycleSettlementTimeoutMs: PROJECT_TASK_LIFECYCLE_SETTLEMENT_TIMEOUT_MS
|
|
23572
|
+
}
|
|
23573
|
+
);
|
|
23217
23574
|
const observer = new ProjectTaskObserver({
|
|
23218
23575
|
tasks: input.taskStore,
|
|
23219
23576
|
workItems: input.taskWorkItemStore,
|
|
@@ -25091,6 +25448,40 @@ var _AgentTaskConsumer = class _AgentTaskConsumer {
|
|
|
25091
25448
|
_AgentTaskConsumer.agent_run_endpoint = "http://localhost:4001/api/runs";
|
|
25092
25449
|
var AgentTaskConsumer = _AgentTaskConsumer;
|
|
25093
25450
|
|
|
25451
|
+
// src/services/gracefulShutdown.ts
|
|
25452
|
+
function createGracefulShutdown(options) {
|
|
25453
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
25454
|
+
const exit = options.exit ?? ((code) => process.exit(code));
|
|
25455
|
+
let started = false;
|
|
25456
|
+
let exited = false;
|
|
25457
|
+
const doExit = (code) => {
|
|
25458
|
+
if (exited) return;
|
|
25459
|
+
exited = true;
|
|
25460
|
+
exit(code);
|
|
25461
|
+
};
|
|
25462
|
+
return async (signal) => {
|
|
25463
|
+
if (started) return;
|
|
25464
|
+
started = true;
|
|
25465
|
+
options.log?.info(`Received ${signal}; shutting down gracefully`, { signal });
|
|
25466
|
+
const timer = setTimeout(() => {
|
|
25467
|
+
options.log?.warn("Graceful shutdown timed out; forcing exit", { signal, timeoutMs });
|
|
25468
|
+
doExit(1);
|
|
25469
|
+
}, timeoutMs);
|
|
25470
|
+
timer.unref?.();
|
|
25471
|
+
for (const step of options.steps) {
|
|
25472
|
+
try {
|
|
25473
|
+
await step();
|
|
25474
|
+
} catch (error) {
|
|
25475
|
+
options.log?.warn("Graceful shutdown step failed", {
|
|
25476
|
+
error: error instanceof Error ? error.message : String(error)
|
|
25477
|
+
});
|
|
25478
|
+
}
|
|
25479
|
+
}
|
|
25480
|
+
clearTimeout(timer);
|
|
25481
|
+
doExit(0);
|
|
25482
|
+
};
|
|
25483
|
+
}
|
|
25484
|
+
|
|
25094
25485
|
// src/index.ts
|
|
25095
25486
|
import {
|
|
25096
25487
|
registerLoggerLattice,
|
|
@@ -25106,7 +25497,7 @@ import {
|
|
|
25106
25497
|
createTrustedAgentMessageDispatcher as createTrustedAgentMessageDispatcher2,
|
|
25107
25498
|
runAsLeader,
|
|
25108
25499
|
configureCoordinationLeaseStore,
|
|
25109
|
-
getReplicaId as
|
|
25500
|
+
getReplicaId as getReplicaId7,
|
|
25110
25501
|
taskLifecycleNotifier as taskLifecycleNotifier3,
|
|
25111
25502
|
projectTaskAdmissionNotifier as projectTaskAdmissionNotifier2,
|
|
25112
25503
|
scheduleLatticeManager as scheduleLatticeManager2,
|
|
@@ -25115,7 +25506,9 @@ import {
|
|
|
25115
25506
|
agentLatticeManager as agentLatticeManager5,
|
|
25116
25507
|
configurePluginCatalogSync,
|
|
25117
25508
|
PLUGIN_CATALOG_CHANGED_TOPIC,
|
|
25118
|
-
getSandboxPluginCatalog as getSandboxPluginCatalog3
|
|
25509
|
+
getSandboxPluginCatalog as getSandboxPluginCatalog3,
|
|
25510
|
+
getSandBoxManager as getSandBoxManager4,
|
|
25511
|
+
installPluginHost
|
|
25119
25512
|
} from "@axiom-lattice/core";
|
|
25120
25513
|
|
|
25121
25514
|
// src/channels/ChannelInfrastructure.ts
|
|
@@ -25223,7 +25616,7 @@ function validateRoomTrustedDispatch(message, options, binding, agentId) {
|
|
|
25223
25616
|
"inputMessageId",
|
|
25224
25617
|
"role",
|
|
25225
25618
|
"title"
|
|
25226
|
-
], ["responsibility", "source"]);
|
|
25619
|
+
], ["responsibility", "source", "rosterExempt"]);
|
|
25227
25620
|
if (!binding || !targetEnvelope || !target || !context || !authority || typeof authority.expectedRoomThreadId !== "string" || authority.expectedRoomThreadId.length === 0 || trusted?.executionMode !== "followup") return false;
|
|
25228
25621
|
const expectedSender = `room:${context.roomId}:membership:${context.membershipId}`;
|
|
25229
25622
|
return targetEnvelope.adapterChannel === "room" && targetEnvelope.channelInstallationId === message.channelInstallationId && target.sourceRoomMessageId === context.sourceRoomMessageId && target.targetMembershipId === context.membershipId && target.inputMessageId === context.inputMessageId && message.tenantId === context.tenantId && message.sender.id === expectedSender && message.conversation?.id === context.roomId && message.conversation?.type === "group" && binding.tenantId === context.tenantId && binding.channel === "room" && binding.channelInstallationId === message.channelInstallationId && binding.senderId === expectedSender && binding.agentId === context.assistantId && binding.workspaceId === context.workspaceId && binding.projectId === context.projectId && binding.threadId === authority.expectedRoomThreadId && binding.threadMode === "fixed" && agentId === context.assistantId;
|
|
@@ -25784,9 +26177,942 @@ async function prepareProjectTaskRuntime(runtime, log2) {
|
|
|
25784
26177
|
}
|
|
25785
26178
|
}
|
|
25786
26179
|
|
|
26180
|
+
// src/services/plugins/PluginDeploymentRuntimeStartup.ts
|
|
26181
|
+
import { setPluginDeploymentService } from "@axiom-lattice/core";
|
|
26182
|
+
|
|
26183
|
+
// src/services/plugins/PluginDeploymentService.ts
|
|
26184
|
+
import { randomUUID as randomUUID28 } from "crypto";
|
|
26185
|
+
import {
|
|
26186
|
+
PLUGIN_HOST_DIR,
|
|
26187
|
+
PluginDeploymentRuntimeRegistry
|
|
26188
|
+
} from "@axiom-lattice/core";
|
|
26189
|
+
|
|
26190
|
+
// src/services/plugins/pluginDeploymentNaming.ts
|
|
26191
|
+
import { createHash as createHash10 } from "crypto";
|
|
26192
|
+
var PLUGIN_GUEST_PORT = 8080;
|
|
26193
|
+
var SANDBOX_NAME_HASH_LENGTH = 16;
|
|
26194
|
+
function pluginSandboxName(tenantId, pluginType) {
|
|
26195
|
+
if (!tenantId || typeof tenantId !== "string") {
|
|
26196
|
+
throw new Error("pluginSandboxName: tenantId must be a non-empty string");
|
|
26197
|
+
}
|
|
26198
|
+
if (!pluginType || typeof pluginType !== "string") {
|
|
26199
|
+
throw new Error("pluginSandboxName: pluginType must be a non-empty string");
|
|
26200
|
+
}
|
|
26201
|
+
const hash = createHash10("sha256").update(`${tenantId}\0${pluginType}`).digest("hex").slice(0, SANDBOX_NAME_HASH_LENGTH);
|
|
26202
|
+
return `plugin-${hash}`;
|
|
26203
|
+
}
|
|
26204
|
+
|
|
26205
|
+
// src/services/plugins/PluginDeploymentService.ts
|
|
26206
|
+
var PLUGIN_HOST_SERVER_PATH = `${PLUGIN_HOST_DIR}/server.js`;
|
|
26207
|
+
var DEFAULT_PLUGIN_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
26208
|
+
var DEFAULT_PLUGIN_HEALTH_TIMEOUT_MS = 3e4;
|
|
26209
|
+
var DEFAULT_PLUGIN_HEALTH_POLL_INTERVAL_MS = 500;
|
|
26210
|
+
var DEFAULT_PLUGIN_HEALTH_PROBE_TIMEOUT_MS = 5e3;
|
|
26211
|
+
var PLUGIN_DEPLOYMENT_LEASE_KEY = "plugin-deployment:reconcile";
|
|
26212
|
+
var PLUGIN_DEPLOYMENT_LEASE_TTL_MS = 6e4;
|
|
26213
|
+
var PLUGIN_TYPE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
26214
|
+
var PluginDeploymentError = class extends Error {
|
|
26215
|
+
/**
|
|
26216
|
+
* @param code - Machine-readable failure code.
|
|
26217
|
+
* @param message - Human-readable description.
|
|
26218
|
+
*/
|
|
26219
|
+
constructor(code, message) {
|
|
26220
|
+
super(message);
|
|
26221
|
+
this.name = "PluginDeploymentError";
|
|
26222
|
+
this.code = code;
|
|
26223
|
+
}
|
|
26224
|
+
};
|
|
26225
|
+
function isRecord4(value) {
|
|
26226
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26227
|
+
}
|
|
26228
|
+
function buildBootstrapEntrypoint(pluginType) {
|
|
26229
|
+
return [
|
|
26230
|
+
"sh",
|
|
26231
|
+
"-c",
|
|
26232
|
+
`while [ ! -f ${PLUGIN_HOST_SERVER_PATH} ]; do sleep 0.2; done; exec node ${PLUGIN_HOST_SERVER_PATH} --type ${pluginType} --port ${PLUGIN_GUEST_PORT}`
|
|
26233
|
+
];
|
|
26234
|
+
}
|
|
26235
|
+
var PluginDeploymentService = class {
|
|
26236
|
+
/**
|
|
26237
|
+
* @param deps - Injected store, sandbox manager, catalog, installer, lease, and seams.
|
|
26238
|
+
*/
|
|
26239
|
+
constructor(deps) {
|
|
26240
|
+
this.store = deps.store;
|
|
26241
|
+
this.sandboxManager = deps.sandboxManager;
|
|
26242
|
+
this.catalog = deps.catalog;
|
|
26243
|
+
this.installPluginHost = deps.installPluginHost;
|
|
26244
|
+
this.lease = deps.lease;
|
|
26245
|
+
this.registry = deps.registry ?? new PluginDeploymentRuntimeRegistry({ now: deps.clock ?? Date.now });
|
|
26246
|
+
this.now = deps.clock ?? Date.now;
|
|
26247
|
+
this.logger = deps.logger ?? { info: () => void 0 };
|
|
26248
|
+
this.limits = deps.limits ?? {};
|
|
26249
|
+
this.serviceBaseUrl = (deps.serviceBaseUrl ?? process.env.MICROSANDBOX_SERVICE_URL ?? "").replace(
|
|
26250
|
+
/\/$/,
|
|
26251
|
+
""
|
|
26252
|
+
);
|
|
26253
|
+
this.serviceKey = deps.serviceKey;
|
|
26254
|
+
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
26255
|
+
this.sleep = deps.sleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
|
|
26256
|
+
this.defaultMode = deps.defaultMode ?? "onDemand";
|
|
26257
|
+
this.defaultIdleTimeoutMs = deps.defaultIdleTimeoutMs ?? DEFAULT_PLUGIN_IDLE_TIMEOUT_MS;
|
|
26258
|
+
this.providerName = deps.providerName ?? "default";
|
|
26259
|
+
this.image = deps.image;
|
|
26260
|
+
this.healthTimeoutMs = deps.healthTimeoutMs ?? DEFAULT_PLUGIN_HEALTH_TIMEOUT_MS;
|
|
26261
|
+
this.healthPollIntervalMs = deps.healthPollIntervalMs ?? DEFAULT_PLUGIN_HEALTH_POLL_INTERVAL_MS;
|
|
26262
|
+
this.healthProbeTimeoutMs = deps.healthProbeTimeoutMs ?? DEFAULT_PLUGIN_HEALTH_PROBE_TIMEOUT_MS;
|
|
26263
|
+
this.replicaId = deps.replicaId ?? String(process.pid);
|
|
26264
|
+
this.listAllDeployments = deps.listAllDeployments;
|
|
26265
|
+
}
|
|
26266
|
+
/**
|
|
26267
|
+
* Provision a resident sandbox for a published tenant plugin.
|
|
26268
|
+
*
|
|
26269
|
+
* Validates the plugin against the tenant catalog, enforces limits, resolves a
|
|
26270
|
+
* deterministic sandbox with the bootstrap entrypoint and exposed guest port,
|
|
26271
|
+
* installs the host server, waits for `/health`, and upserts a `running`
|
|
26272
|
+
* {@link PluginDeployment}. Re-deploying an existing deployment reuses the same
|
|
26273
|
+
* sandbox name and record (idempotent).
|
|
26274
|
+
*
|
|
26275
|
+
* @param tenantId - Tenant that owns the plugin.
|
|
26276
|
+
* @param pluginType - Plugin type (directory key).
|
|
26277
|
+
* @param options - Lifecycle mode and overrides.
|
|
26278
|
+
* @returns The stored deployment record.
|
|
26279
|
+
* @throws PluginDeploymentError with `PLUGIN_NOT_FOUND`, `INVALID_PLUGIN_TYPE`,
|
|
26280
|
+
* `LIMIT_EXCEEDED`, `BACKEND_UNSUPPORTED`, `SANDBOX_CREATE_FAILED`, or
|
|
26281
|
+
* `HEALTH_TIMEOUT`.
|
|
26282
|
+
*/
|
|
26283
|
+
async deploy(tenantId, pluginType, options = {}) {
|
|
26284
|
+
const snapshot = await this.catalog.ensureLoaded(tenantId);
|
|
26285
|
+
const manifest = snapshot.manifests[pluginType];
|
|
26286
|
+
if (!manifest) {
|
|
26287
|
+
throw new PluginDeploymentError(
|
|
26288
|
+
"PLUGIN_NOT_FOUND",
|
|
26289
|
+
`plugin "${pluginType}" is not published for tenant "${tenantId}"`
|
|
26290
|
+
);
|
|
26291
|
+
}
|
|
26292
|
+
if (!PLUGIN_TYPE_RE.test(pluginType)) {
|
|
26293
|
+
throw new PluginDeploymentError(
|
|
26294
|
+
"INVALID_PLUGIN_TYPE",
|
|
26295
|
+
`plugin type "${pluginType}" is not a valid kebab-case identifier`
|
|
26296
|
+
);
|
|
26297
|
+
}
|
|
26298
|
+
const existing = await this.store.get(tenantId, pluginType);
|
|
26299
|
+
if (!existing) {
|
|
26300
|
+
await this.assertWithinLimits(tenantId);
|
|
26301
|
+
}
|
|
26302
|
+
const mode = options.mode ?? existing?.mode ?? this.defaultMode;
|
|
26303
|
+
const idleTimeoutMs = options.idleTimeoutMs ?? existing?.idleTimeoutMs ?? this.defaultIdleTimeoutMs;
|
|
26304
|
+
const name = pluginSandboxName(tenantId, pluginType);
|
|
26305
|
+
const config = this.buildSandboxConfig(
|
|
26306
|
+
tenantId,
|
|
26307
|
+
pluginType,
|
|
26308
|
+
mode,
|
|
26309
|
+
idleTimeoutMs,
|
|
26310
|
+
options.image ?? this.image
|
|
26311
|
+
);
|
|
26312
|
+
let sandbox;
|
|
26313
|
+
try {
|
|
26314
|
+
sandbox = await this.sandboxManager.createSandbox(name, config);
|
|
26315
|
+
await this.installPluginHost(sandbox);
|
|
26316
|
+
} catch (error) {
|
|
26317
|
+
throw new PluginDeploymentError(
|
|
26318
|
+
"SANDBOX_CREATE_FAILED",
|
|
26319
|
+
`failed to provision sandbox "${name}": ${errorMessage4(error)}`
|
|
26320
|
+
);
|
|
26321
|
+
}
|
|
26322
|
+
const endpoint = await this.resolveEndpoint(sandbox);
|
|
26323
|
+
const health = await this.waitForHealth(endpoint);
|
|
26324
|
+
if (!health.ok) {
|
|
26325
|
+
const failedAt = this.nowIso();
|
|
26326
|
+
await this.store.upsert(
|
|
26327
|
+
this.buildRecord({
|
|
26328
|
+
id: existing?.id ?? randomUUID28(),
|
|
26329
|
+
tenantId,
|
|
26330
|
+
pluginType,
|
|
26331
|
+
pluginVersion: manifest.version,
|
|
26332
|
+
mode,
|
|
26333
|
+
status: "failed",
|
|
26334
|
+
sandboxName: name,
|
|
26335
|
+
endpoint,
|
|
26336
|
+
idleTimeoutMs,
|
|
26337
|
+
provider: options.provider ?? this.providerName,
|
|
26338
|
+
image: options.image ?? this.image,
|
|
26339
|
+
lastHealthAt: failedAt,
|
|
26340
|
+
lastError: health.error,
|
|
26341
|
+
createdAt: existing?.createdAt
|
|
26342
|
+
})
|
|
26343
|
+
);
|
|
26344
|
+
throw new PluginDeploymentError(
|
|
26345
|
+
"HEALTH_TIMEOUT",
|
|
26346
|
+
`resident host for plugin "${pluginType}" did not become healthy: ${health.error ?? "unknown"}`
|
|
26347
|
+
);
|
|
26348
|
+
}
|
|
26349
|
+
const record = this.buildRecord({
|
|
26350
|
+
id: existing?.id ?? randomUUID28(),
|
|
26351
|
+
tenantId,
|
|
26352
|
+
pluginType,
|
|
26353
|
+
pluginVersion: manifest.version,
|
|
26354
|
+
mode,
|
|
26355
|
+
status: "running",
|
|
26356
|
+
sandboxName: name,
|
|
26357
|
+
endpoint,
|
|
26358
|
+
idleTimeoutMs,
|
|
26359
|
+
provider: options.provider ?? this.providerName,
|
|
26360
|
+
image: options.image ?? this.image,
|
|
26361
|
+
lastHealthAt: this.nowIso(),
|
|
26362
|
+
createdAt: existing?.createdAt
|
|
26363
|
+
});
|
|
26364
|
+
const stored = await this.store.upsert(record);
|
|
26365
|
+
const key = this.registryKey(tenantId, pluginType);
|
|
26366
|
+
this.registry.setEndpoint(key, endpoint);
|
|
26367
|
+
this.registry.markHealthy(key);
|
|
26368
|
+
this.logger.info(
|
|
26369
|
+
{ event: "plugin.deployment.deployed", tenantId, pluginType, mode, endpoint },
|
|
26370
|
+
"plugin deployment running"
|
|
26371
|
+
);
|
|
26372
|
+
return stored;
|
|
26373
|
+
}
|
|
26374
|
+
/**
|
|
26375
|
+
* Delete a deployment's sandbox and record.
|
|
26376
|
+
*
|
|
26377
|
+
* Sandbox deletion is best-effort: a provider failure is logged but does not
|
|
26378
|
+
* prevent the record from being removed.
|
|
26379
|
+
*
|
|
26380
|
+
* @param tenantId - Tenant that owns the plugin.
|
|
26381
|
+
* @param pluginType - Plugin type.
|
|
26382
|
+
* @returns Whether a deployment record was removed.
|
|
26383
|
+
*/
|
|
26384
|
+
async undeploy(tenantId, pluginType) {
|
|
26385
|
+
const name = pluginSandboxName(tenantId, pluginType);
|
|
26386
|
+
try {
|
|
26387
|
+
await this.sandboxManager.deleteSandbox(name);
|
|
26388
|
+
} catch (error) {
|
|
26389
|
+
this.logger.warn?.(
|
|
26390
|
+
{ event: "plugin.deployment.undeploy.sandbox_failed", tenantId, pluginType, error: errorMessage4(error) },
|
|
26391
|
+
"best-effort sandbox delete failed"
|
|
26392
|
+
);
|
|
26393
|
+
}
|
|
26394
|
+
const undeployed = await this.store.delete(tenantId, pluginType);
|
|
26395
|
+
this.registry.clear(this.registryKey(tenantId, pluginType));
|
|
26396
|
+
this.logger.info(
|
|
26397
|
+
{ event: "plugin.deployment.undeployed", tenantId, pluginType, undeployed },
|
|
26398
|
+
"plugin deployment removed"
|
|
26399
|
+
);
|
|
26400
|
+
return { undeployed };
|
|
26401
|
+
}
|
|
26402
|
+
/**
|
|
26403
|
+
* Read the durable deployment status and refresh it with a live `/health`.
|
|
26404
|
+
*
|
|
26405
|
+
* When a `running` record exists, a single `/health` probe updates
|
|
26406
|
+
* `lastHealthAt`/`lastError`; the probe never throws.
|
|
26407
|
+
*
|
|
26408
|
+
* @param tenantId - Tenant that owns the plugin.
|
|
26409
|
+
* @param pluginType - Plugin type.
|
|
26410
|
+
* @returns The status view (with `status: "absent"` when no record exists).
|
|
26411
|
+
*/
|
|
26412
|
+
async status(tenantId, pluginType) {
|
|
26413
|
+
const record = await this.store.get(tenantId, pluginType);
|
|
26414
|
+
if (!record) {
|
|
26415
|
+
return { status: "absent" };
|
|
26416
|
+
}
|
|
26417
|
+
let lastHealthAt = record.lastHealthAt;
|
|
26418
|
+
let lastError = record.lastError;
|
|
26419
|
+
if (record.status === "running" && record.endpoint) {
|
|
26420
|
+
const health = await this.probeHealth(record.endpoint);
|
|
26421
|
+
const key = this.registryKey(tenantId, pluginType);
|
|
26422
|
+
if (health.ok) {
|
|
26423
|
+
this.registry.markHealthy(key);
|
|
26424
|
+
lastHealthAt = this.nowIso();
|
|
26425
|
+
lastError = void 0;
|
|
26426
|
+
} else {
|
|
26427
|
+
this.registry.markUnhealthy(key, health.error ?? "health probe failed");
|
|
26428
|
+
lastHealthAt = this.nowIso();
|
|
26429
|
+
lastError = health.error ?? "health probe failed";
|
|
26430
|
+
}
|
|
26431
|
+
await this.store.updateStatus(tenantId, pluginType, { lastHealthAt, lastError });
|
|
26432
|
+
}
|
|
26433
|
+
return {
|
|
26434
|
+
status: record.status,
|
|
26435
|
+
endpoint: record.endpoint,
|
|
26436
|
+
mode: record.mode,
|
|
26437
|
+
lastHealthAt,
|
|
26438
|
+
...lastError === void 0 ? {} : { lastError },
|
|
26439
|
+
sandboxName: record.sandboxName,
|
|
26440
|
+
port: record.port
|
|
26441
|
+
};
|
|
26442
|
+
}
|
|
26443
|
+
/**
|
|
26444
|
+
* List every resident deployment for a tenant.
|
|
26445
|
+
*
|
|
26446
|
+
* Reads the durable store directly (no `/health` probe) and maps each record
|
|
26447
|
+
* to the minimal view the read-only `manage_plugin deployments` MCP App needs.
|
|
26448
|
+
*
|
|
26449
|
+
* @param tenantId - Tenant whose deployments are listed.
|
|
26450
|
+
* @returns One minimal entry per stored deployment.
|
|
26451
|
+
*/
|
|
26452
|
+
async list(tenantId) {
|
|
26453
|
+
const records = await this.store.listByTenant(tenantId);
|
|
26454
|
+
return records.map((record) => ({
|
|
26455
|
+
pluginType: record.pluginType,
|
|
26456
|
+
status: record.status,
|
|
26457
|
+
mode: record.mode,
|
|
26458
|
+
...record.endpoint === void 0 ? {} : { endpoint: record.endpoint },
|
|
26459
|
+
...record.lastHealthAt === void 0 ? {} : { lastHealthAt: record.lastHealthAt },
|
|
26460
|
+
...record.lastError === void 0 ? {} : { lastError: record.lastError }
|
|
26461
|
+
}));
|
|
26462
|
+
}
|
|
26463
|
+
/**
|
|
26464
|
+
* Read a deployment's stored record without probing its host.
|
|
26465
|
+
*
|
|
26466
|
+
* The catalog executor uses this to decide whether a plugin tool call should
|
|
26467
|
+
* route to the resident service. Unlike {@link status} it never issues a
|
|
26468
|
+
* `/health` probe and resolves `undefined` when no record exists.
|
|
26469
|
+
*
|
|
26470
|
+
* @param tenantId - Tenant that owns the plugin.
|
|
26471
|
+
* @param pluginType - Plugin type.
|
|
26472
|
+
* @returns The minimal `{ status, endpoint, mode }` view, or `undefined`.
|
|
26473
|
+
*/
|
|
26474
|
+
async getDeployment(tenantId, pluginType) {
|
|
26475
|
+
const record = await this.store.get(tenantId, pluginType);
|
|
26476
|
+
if (!record) return void 0;
|
|
26477
|
+
return { status: record.status, endpoint: record.endpoint, mode: record.mode };
|
|
26478
|
+
}
|
|
26479
|
+
/**
|
|
26480
|
+
* Ensure an existing on-demand deployment is running and healthy.
|
|
26481
|
+
*
|
|
26482
|
+
* Paused/stopped deployments are resumed (or re-provisioned when the sandbox
|
|
26483
|
+
* is gone) and their status is refreshed. Concurrent calls for the same plugin
|
|
26484
|
+
* are coalesced by the runtime registry's single-flight.
|
|
26485
|
+
*
|
|
26486
|
+
* @param tenantId - Tenant that owns the plugin.
|
|
26487
|
+
* @param pluginType - Plugin type.
|
|
26488
|
+
*/
|
|
26489
|
+
async ensureRunning(tenantId, pluginType) {
|
|
26490
|
+
const record = await this.store.get(tenantId, pluginType);
|
|
26491
|
+
if (!record) return;
|
|
26492
|
+
if (record.status === "running" && record.endpoint) return;
|
|
26493
|
+
const key = this.registryKey(tenantId, pluginType);
|
|
26494
|
+
await this.registry.ensureRunning(key, async () => {
|
|
26495
|
+
const config = this.buildSandboxConfig(
|
|
26496
|
+
tenantId,
|
|
26497
|
+
pluginType,
|
|
26498
|
+
record.mode,
|
|
26499
|
+
record.idleTimeoutMs,
|
|
26500
|
+
record.imageRef ?? this.image
|
|
26501
|
+
);
|
|
26502
|
+
let sandbox;
|
|
26503
|
+
try {
|
|
26504
|
+
sandbox = await this.sandboxManager.getSandbox(record.sandboxName);
|
|
26505
|
+
await sandbox.start();
|
|
26506
|
+
} catch {
|
|
26507
|
+
try {
|
|
26508
|
+
sandbox = await this.sandboxManager.createSandbox(record.sandboxName, config);
|
|
26509
|
+
await this.installPluginHost(sandbox);
|
|
26510
|
+
} catch (error) {
|
|
26511
|
+
throw new PluginDeploymentError(
|
|
26512
|
+
"SANDBOX_CREATE_FAILED",
|
|
26513
|
+
`failed to provision sandbox "${record.sandboxName}": ${errorMessage4(error)}`
|
|
26514
|
+
);
|
|
26515
|
+
}
|
|
26516
|
+
}
|
|
26517
|
+
const endpoint = await this.resolveEndpoint(sandbox);
|
|
26518
|
+
const health = await this.waitForHealth(endpoint);
|
|
26519
|
+
if (!health.ok) {
|
|
26520
|
+
await this.store.updateStatus(tenantId, pluginType, {
|
|
26521
|
+
status: "failed",
|
|
26522
|
+
endpoint,
|
|
26523
|
+
lastHealthAt: this.nowIso(),
|
|
26524
|
+
lastError: health.error ?? "health probe failed"
|
|
26525
|
+
});
|
|
26526
|
+
throw new PluginDeploymentError(
|
|
26527
|
+
"HEALTH_TIMEOUT",
|
|
26528
|
+
`resident host for plugin "${pluginType}" did not become healthy: ${health.error ?? "unknown"}`
|
|
26529
|
+
);
|
|
26530
|
+
}
|
|
26531
|
+
await this.store.updateStatus(tenantId, pluginType, {
|
|
26532
|
+
status: "running",
|
|
26533
|
+
endpoint,
|
|
26534
|
+
lastHealthAt: this.nowIso(),
|
|
26535
|
+
lastError: void 0
|
|
26536
|
+
});
|
|
26537
|
+
this.registry.setEndpoint(key, endpoint);
|
|
26538
|
+
this.registry.markHealthy(key);
|
|
26539
|
+
});
|
|
26540
|
+
}
|
|
26541
|
+
/**
|
|
26542
|
+
* Reconcile the store against the provider at startup.
|
|
26543
|
+
*
|
|
26544
|
+
* Lease-gated and never throws: for each stored deployment it verifies the
|
|
26545
|
+
* sandbox still exists, refreshes the endpoint, and marks missing/unreadable
|
|
26546
|
+
* sandboxes `failed` with a diagnostic. Runs are best-effort.
|
|
26547
|
+
*
|
|
26548
|
+
* @returns A summary of the pass.
|
|
26549
|
+
*/
|
|
26550
|
+
async startupReconcile() {
|
|
26551
|
+
if (!await this.acquireLease()) {
|
|
26552
|
+
return { ran: false, checked: 0, refreshed: 0, failed: 0 };
|
|
26553
|
+
}
|
|
26554
|
+
const records = await this.listAll();
|
|
26555
|
+
let refreshed = 0;
|
|
26556
|
+
let failed = 0;
|
|
26557
|
+
for (const record of records) {
|
|
26558
|
+
try {
|
|
26559
|
+
const sandbox = await this.sandboxManager.getSandbox(record.sandboxName);
|
|
26560
|
+
const sandboxStatus = await sandbox.getStatus();
|
|
26561
|
+
if (sandboxStatus === "unknown") {
|
|
26562
|
+
throw new Error("sandbox not found");
|
|
26563
|
+
}
|
|
26564
|
+
const endpoint = typeof sandbox.getEndpoint === "function" ? (await sandbox.getEndpoint(record.port || PLUGIN_GUEST_PORT)).endpoint : record.endpoint;
|
|
26565
|
+
await this.store.updateStatus(record.tenantId, record.pluginType, {
|
|
26566
|
+
status: sandboxStatus === "running" ? "running" : record.status,
|
|
26567
|
+
...endpoint === void 0 ? {} : { endpoint },
|
|
26568
|
+
lastError: void 0
|
|
26569
|
+
});
|
|
26570
|
+
if (endpoint !== void 0) {
|
|
26571
|
+
this.registry.setEndpoint(this.registryKey(record.tenantId, record.pluginType), endpoint);
|
|
26572
|
+
}
|
|
26573
|
+
refreshed += 1;
|
|
26574
|
+
} catch (error) {
|
|
26575
|
+
failed += 1;
|
|
26576
|
+
this.logger.warn?.(
|
|
26577
|
+
{
|
|
26578
|
+
event: "plugin.deployment.reconcile.failed",
|
|
26579
|
+
tenantId: record.tenantId,
|
|
26580
|
+
pluginType: record.pluginType,
|
|
26581
|
+
sandboxName: record.sandboxName,
|
|
26582
|
+
error: errorMessage4(error)
|
|
26583
|
+
},
|
|
26584
|
+
"stored plugin deployment has no reachable sandbox"
|
|
26585
|
+
);
|
|
26586
|
+
await this.safeUpdateStatus(record.tenantId, record.pluginType, {
|
|
26587
|
+
status: "failed",
|
|
26588
|
+
lastError: `startup reconcile: ${errorMessage4(error)}`
|
|
26589
|
+
});
|
|
26590
|
+
}
|
|
26591
|
+
}
|
|
26592
|
+
this.logger.info(
|
|
26593
|
+
{ event: "plugin.deployment.reconcile", checked: records.length, refreshed, failed },
|
|
26594
|
+
"plugin deployment startup reconcile complete"
|
|
26595
|
+
);
|
|
26596
|
+
return { ran: true, checked: records.length, refreshed, failed };
|
|
26597
|
+
}
|
|
26598
|
+
/**
|
|
26599
|
+
* Probe every running deployment's `/health` and refresh its health record.
|
|
26600
|
+
*
|
|
26601
|
+
* Lease-gated and never throws. Unhealthy `alwaysOn` deployments are
|
|
26602
|
+
* re-provisioned best-effort.
|
|
26603
|
+
*
|
|
26604
|
+
* @returns A summary of the pass.
|
|
26605
|
+
*/
|
|
26606
|
+
async healthWatchdog() {
|
|
26607
|
+
if (!await this.acquireLease()) {
|
|
26608
|
+
return { ran: false, checked: 0, healthy: 0, unhealthy: 0 };
|
|
26609
|
+
}
|
|
26610
|
+
const records = await this.listAll();
|
|
26611
|
+
let healthy = 0;
|
|
26612
|
+
let unhealthy = 0;
|
|
26613
|
+
for (const record of records) {
|
|
26614
|
+
if (record.status !== "running" || !record.endpoint) continue;
|
|
26615
|
+
const key = this.registryKey(record.tenantId, record.pluginType);
|
|
26616
|
+
const health = await this.probeHealth(record.endpoint);
|
|
26617
|
+
if (health.ok) {
|
|
26618
|
+
healthy += 1;
|
|
26619
|
+
this.registry.markHealthy(key);
|
|
26620
|
+
await this.safeUpdateStatus(record.tenantId, record.pluginType, {
|
|
26621
|
+
lastHealthAt: this.nowIso(),
|
|
26622
|
+
lastError: void 0
|
|
26623
|
+
});
|
|
26624
|
+
continue;
|
|
26625
|
+
}
|
|
26626
|
+
unhealthy += 1;
|
|
26627
|
+
this.registry.markUnhealthy(key, health.error ?? "health probe failed");
|
|
26628
|
+
await this.safeUpdateStatus(record.tenantId, record.pluginType, {
|
|
26629
|
+
lastHealthAt: this.nowIso(),
|
|
26630
|
+
lastError: health.error ?? "health probe failed"
|
|
26631
|
+
});
|
|
26632
|
+
if (record.mode === "alwaysOn") {
|
|
26633
|
+
await this.restartBestEffort(record, key);
|
|
26634
|
+
}
|
|
26635
|
+
}
|
|
26636
|
+
this.logger.info(
|
|
26637
|
+
{ event: "plugin.deployment.watchdog", checked: healthy + unhealthy, healthy, unhealthy },
|
|
26638
|
+
"plugin deployment health watchdog complete"
|
|
26639
|
+
);
|
|
26640
|
+
return { ran: true, checked: healthy + unhealthy, healthy, unhealthy };
|
|
26641
|
+
}
|
|
26642
|
+
/**
|
|
26643
|
+
* Best-effort re-provision and health-check of an unhealthy `alwaysOn`
|
|
26644
|
+
* deployment.
|
|
26645
|
+
*
|
|
26646
|
+
* @param record - Deployment to restart.
|
|
26647
|
+
* @param key - Runtime registry key.
|
|
26648
|
+
*/
|
|
26649
|
+
async restartBestEffort(record, key) {
|
|
26650
|
+
try {
|
|
26651
|
+
const config = this.buildSandboxConfig(
|
|
26652
|
+
record.tenantId,
|
|
26653
|
+
record.pluginType,
|
|
26654
|
+
record.mode,
|
|
26655
|
+
record.idleTimeoutMs,
|
|
26656
|
+
record.imageRef ?? this.image
|
|
26657
|
+
);
|
|
26658
|
+
let sandbox;
|
|
26659
|
+
try {
|
|
26660
|
+
sandbox = await this.sandboxManager.createSandbox(record.sandboxName, config);
|
|
26661
|
+
await sandbox.start().catch(() => void 0);
|
|
26662
|
+
await this.installPluginHost(sandbox);
|
|
26663
|
+
} catch (error) {
|
|
26664
|
+
throw new PluginDeploymentError(
|
|
26665
|
+
"SANDBOX_CREATE_FAILED",
|
|
26666
|
+
`failed to provision sandbox "${record.sandboxName}": ${errorMessage4(error)}`
|
|
26667
|
+
);
|
|
26668
|
+
}
|
|
26669
|
+
const endpoint = await this.resolveEndpoint(sandbox);
|
|
26670
|
+
const health = await this.waitForHealth(endpoint);
|
|
26671
|
+
if (!health.ok) {
|
|
26672
|
+
await this.safeUpdateStatus(record.tenantId, record.pluginType, {
|
|
26673
|
+
endpoint,
|
|
26674
|
+
lastError: health.error ?? "health probe failed"
|
|
26675
|
+
});
|
|
26676
|
+
return;
|
|
26677
|
+
}
|
|
26678
|
+
await this.safeUpdateStatus(record.tenantId, record.pluginType, {
|
|
26679
|
+
status: "running",
|
|
26680
|
+
endpoint,
|
|
26681
|
+
lastHealthAt: this.nowIso(),
|
|
26682
|
+
lastError: void 0
|
|
26683
|
+
});
|
|
26684
|
+
this.registry.setEndpoint(key, endpoint);
|
|
26685
|
+
this.registry.markHealthy(key);
|
|
26686
|
+
} catch (error) {
|
|
26687
|
+
this.logger.warn?.(
|
|
26688
|
+
{
|
|
26689
|
+
event: "plugin.deployment.watchdog.restart_failed",
|
|
26690
|
+
tenantId: record.tenantId,
|
|
26691
|
+
pluginType: record.pluginType,
|
|
26692
|
+
error: errorMessage4(error)
|
|
26693
|
+
},
|
|
26694
|
+
"best-effort alwaysOn restart failed"
|
|
26695
|
+
);
|
|
26696
|
+
}
|
|
26697
|
+
}
|
|
26698
|
+
/**
|
|
26699
|
+
* Build the sandbox creation config for a plugin deployment.
|
|
26700
|
+
*
|
|
26701
|
+
* @param tenantId - Tenant id.
|
|
26702
|
+
* @param pluginType - Plugin type.
|
|
26703
|
+
* @param mode - Lifecycle strategy.
|
|
26704
|
+
* @param idleTimeoutMs - On-demand idle timeout in milliseconds.
|
|
26705
|
+
* @param image - Optional image override for this provisioning.
|
|
26706
|
+
* @returns The provider-facing config.
|
|
26707
|
+
*/
|
|
26708
|
+
buildSandboxConfig(tenantId, pluginType, mode, idleTimeoutMs, image = this.image) {
|
|
26709
|
+
return {
|
|
26710
|
+
assistant_id: "",
|
|
26711
|
+
thread_id: pluginSandboxName(tenantId, pluginType),
|
|
26712
|
+
tenantId,
|
|
26713
|
+
entrypoint: buildBootstrapEntrypoint(pluginType),
|
|
26714
|
+
exposePorts: [PLUGIN_GUEST_PORT],
|
|
26715
|
+
// `alwaysOn` deployments never expire; on-demand deployments are
|
|
26716
|
+
// time-boxed by the service (finalized with the lifecycle work in
|
|
26717
|
+
// Plan 4 Task 6). The provider expects seconds, not milliseconds.
|
|
26718
|
+
timeoutSeconds: mode === "alwaysOn" ? null : Math.max(1, Math.round(idleTimeoutMs / 1e3)),
|
|
26719
|
+
...image === void 0 ? {} : { image }
|
|
26720
|
+
};
|
|
26721
|
+
}
|
|
26722
|
+
/**
|
|
26723
|
+
* Resolve the routable endpoint for the published guest port.
|
|
26724
|
+
*
|
|
26725
|
+
* @param sandbox - Provisioned sandbox instance.
|
|
26726
|
+
* @returns The relative proxy path or scheme-less host endpoint.
|
|
26727
|
+
* @throws PluginDeploymentError with `BACKEND_UNSUPPORTED` when the provider
|
|
26728
|
+
* does not implement `getEndpoint`.
|
|
26729
|
+
*/
|
|
26730
|
+
async resolveEndpoint(sandbox) {
|
|
26731
|
+
if (typeof sandbox.getEndpoint !== "function") {
|
|
26732
|
+
throw new PluginDeploymentError(
|
|
26733
|
+
"BACKEND_UNSUPPORTED",
|
|
26734
|
+
"sandbox provider does not support resident plugin services (missing getEndpoint)"
|
|
26735
|
+
);
|
|
26736
|
+
}
|
|
26737
|
+
const { endpoint } = await sandbox.getEndpoint(PLUGIN_GUEST_PORT);
|
|
26738
|
+
return endpoint;
|
|
26739
|
+
}
|
|
26740
|
+
/**
|
|
26741
|
+
* Poll `/health` until the host answers healthy or the deadline passes.
|
|
26742
|
+
*
|
|
26743
|
+
* @param endpoint - Resolved endpoint route.
|
|
26744
|
+
* @returns The final probe outcome.
|
|
26745
|
+
*/
|
|
26746
|
+
async waitForHealth(endpoint) {
|
|
26747
|
+
const deadline = Date.now() + this.healthTimeoutMs;
|
|
26748
|
+
let last = { ok: false, error: "health not attempted" };
|
|
26749
|
+
do {
|
|
26750
|
+
last = await this.probeHealth(endpoint);
|
|
26751
|
+
if (last.ok) return last;
|
|
26752
|
+
if (Date.now() >= deadline) break;
|
|
26753
|
+
await this.sleep(this.healthPollIntervalMs);
|
|
26754
|
+
} while (Date.now() < deadline);
|
|
26755
|
+
return last;
|
|
26756
|
+
}
|
|
26757
|
+
/**
|
|
26758
|
+
* Issue a single `GET ${baseUrl}${endpoint}/health` request.
|
|
26759
|
+
*
|
|
26760
|
+
* Never throws; network/timeout/non-2xx/parse failures resolve `{ ok: false }`.
|
|
26761
|
+
*
|
|
26762
|
+
* @param endpoint - Resolved endpoint route.
|
|
26763
|
+
* @returns The probe outcome.
|
|
26764
|
+
*/
|
|
26765
|
+
async probeHealth(endpoint) {
|
|
26766
|
+
const url = `${this.serviceBaseUrl}${endpoint}/health`;
|
|
26767
|
+
const controller = new AbortController();
|
|
26768
|
+
const timer = setTimeout(() => controller.abort(), this.healthProbeTimeoutMs);
|
|
26769
|
+
try {
|
|
26770
|
+
const response = await this.fetchImpl(url, {
|
|
26771
|
+
method: "GET",
|
|
26772
|
+
...this.serviceKey === void 0 ? {} : { headers: { Authorization: `Bearer ${this.serviceKey}` } },
|
|
26773
|
+
signal: controller.signal
|
|
26774
|
+
});
|
|
26775
|
+
if (!response.ok) {
|
|
26776
|
+
return { ok: false, error: `health responded with HTTP ${response.status}` };
|
|
26777
|
+
}
|
|
26778
|
+
const body = await response.json().catch(() => void 0);
|
|
26779
|
+
if (isRecord4(body) && body.ok === true) {
|
|
26780
|
+
return { ok: true };
|
|
26781
|
+
}
|
|
26782
|
+
return { ok: false, error: "health returned an invalid envelope" };
|
|
26783
|
+
} catch (error) {
|
|
26784
|
+
return { ok: false, error: errorMessage4(error) };
|
|
26785
|
+
} finally {
|
|
26786
|
+
clearTimeout(timer);
|
|
26787
|
+
}
|
|
26788
|
+
}
|
|
26789
|
+
/**
|
|
26790
|
+
* Enforce per-tenant and global deployment caps.
|
|
26791
|
+
*
|
|
26792
|
+
* @param tenantId - Tenant attempting to deploy.
|
|
26793
|
+
* @throws PluginDeploymentError with `LIMIT_EXCEEDED`.
|
|
26794
|
+
*/
|
|
26795
|
+
async assertWithinLimits(tenantId) {
|
|
26796
|
+
const { maxPerTenant, maxGlobal } = this.limits;
|
|
26797
|
+
if (maxPerTenant !== void 0) {
|
|
26798
|
+
const tenantRecords = await this.store.listByTenant(tenantId);
|
|
26799
|
+
if (tenantRecords.length >= maxPerTenant) {
|
|
26800
|
+
throw new PluginDeploymentError(
|
|
26801
|
+
"LIMIT_EXCEEDED",
|
|
26802
|
+
`tenant "${tenantId}" reached the resident plugin limit (${maxPerTenant})`
|
|
26803
|
+
);
|
|
26804
|
+
}
|
|
26805
|
+
}
|
|
26806
|
+
if (maxGlobal !== void 0 && this.listAllDeployments) {
|
|
26807
|
+
const all = await this.listAllDeployments();
|
|
26808
|
+
if (all.length >= maxGlobal) {
|
|
26809
|
+
throw new PluginDeploymentError(
|
|
26810
|
+
"LIMIT_EXCEEDED",
|
|
26811
|
+
`global resident plugin limit reached (${maxGlobal})`
|
|
26812
|
+
);
|
|
26813
|
+
}
|
|
26814
|
+
}
|
|
26815
|
+
}
|
|
26816
|
+
/**
|
|
26817
|
+
* Acquire the reconcile lease for this pass.
|
|
26818
|
+
*
|
|
26819
|
+
* @returns True when this replica may run the pass.
|
|
26820
|
+
*/
|
|
26821
|
+
async acquireLease() {
|
|
26822
|
+
try {
|
|
26823
|
+
return await this.lease.tryAcquire(
|
|
26824
|
+
PLUGIN_DEPLOYMENT_LEASE_KEY,
|
|
26825
|
+
this.replicaId,
|
|
26826
|
+
PLUGIN_DEPLOYMENT_LEASE_TTL_MS
|
|
26827
|
+
);
|
|
26828
|
+
} catch (error) {
|
|
26829
|
+
this.logger.warn?.(
|
|
26830
|
+
{ event: "plugin.deployment.lease.failed", error: errorMessage4(error) },
|
|
26831
|
+
"could not acquire reconcile lease; skipping pass"
|
|
26832
|
+
);
|
|
26833
|
+
return false;
|
|
26834
|
+
}
|
|
26835
|
+
}
|
|
26836
|
+
/**
|
|
26837
|
+
* List deployments for reconcile/watchdog, degrading to an empty list.
|
|
26838
|
+
*
|
|
26839
|
+
* @returns Stored deployments, or `[]` when no global lister is wired.
|
|
26840
|
+
*/
|
|
26841
|
+
async listAll() {
|
|
26842
|
+
if (!this.listAllDeployments) return [];
|
|
26843
|
+
try {
|
|
26844
|
+
return await this.listAllDeployments();
|
|
26845
|
+
} catch (error) {
|
|
26846
|
+
this.logger.warn?.(
|
|
26847
|
+
{ event: "plugin.deployment.list.failed", error: errorMessage4(error) },
|
|
26848
|
+
"could not list plugin deployments"
|
|
26849
|
+
);
|
|
26850
|
+
return [];
|
|
26851
|
+
}
|
|
26852
|
+
}
|
|
26853
|
+
/**
|
|
26854
|
+
* Apply a status patch, swallowing store errors (used on the soft paths).
|
|
26855
|
+
*
|
|
26856
|
+
* @param tenantId - Tenant id.
|
|
26857
|
+
* @param pluginType - Plugin type.
|
|
26858
|
+
* @param patch - Partial deployment update.
|
|
26859
|
+
*/
|
|
26860
|
+
async safeUpdateStatus(tenantId, pluginType, patch) {
|
|
26861
|
+
try {
|
|
26862
|
+
await this.store.updateStatus(tenantId, pluginType, patch);
|
|
26863
|
+
} catch (error) {
|
|
26864
|
+
this.logger.warn?.(
|
|
26865
|
+
{
|
|
26866
|
+
event: "plugin.deployment.store_update_failed",
|
|
26867
|
+
tenantId,
|
|
26868
|
+
pluginType,
|
|
26869
|
+
error: errorMessage4(error)
|
|
26870
|
+
},
|
|
26871
|
+
"could not update plugin deployment status"
|
|
26872
|
+
);
|
|
26873
|
+
}
|
|
26874
|
+
}
|
|
26875
|
+
/**
|
|
26876
|
+
* Assemble a durable deployment record.
|
|
26877
|
+
*
|
|
26878
|
+
* @param input - Record fields (optional fields fall back to now/undefined).
|
|
26879
|
+
* @returns A fully populated deployment.
|
|
26880
|
+
*/
|
|
26881
|
+
buildRecord(input) {
|
|
26882
|
+
const now = this.nowIso();
|
|
26883
|
+
return {
|
|
26884
|
+
id: input.id,
|
|
26885
|
+
tenantId: input.tenantId,
|
|
26886
|
+
pluginType: input.pluginType,
|
|
26887
|
+
pluginVersion: input.pluginVersion,
|
|
26888
|
+
mode: input.mode,
|
|
26889
|
+
status: input.status,
|
|
26890
|
+
provider: input.provider,
|
|
26891
|
+
sandboxName: input.sandboxName,
|
|
26892
|
+
port: PLUGIN_GUEST_PORT,
|
|
26893
|
+
endpoint: input.endpoint,
|
|
26894
|
+
...input.image === void 0 ? {} : { imageRef: input.image },
|
|
26895
|
+
idleTimeoutMs: input.idleTimeoutMs,
|
|
26896
|
+
...input.lastHealthAt === void 0 ? {} : { lastHealthAt: input.lastHealthAt },
|
|
26897
|
+
...input.lastError === void 0 ? {} : { lastError: input.lastError },
|
|
26898
|
+
createdAt: input.createdAt ?? now,
|
|
26899
|
+
updatedAt: now
|
|
26900
|
+
};
|
|
26901
|
+
}
|
|
26902
|
+
/**
|
|
26903
|
+
* Compose the runtime registry key for a deployment.
|
|
26904
|
+
*
|
|
26905
|
+
* @param tenantId - Tenant id.
|
|
26906
|
+
* @param pluginType - Plugin type.
|
|
26907
|
+
* @returns `${tenantId}:${pluginType}`.
|
|
26908
|
+
*/
|
|
26909
|
+
registryKey(tenantId, pluginType) {
|
|
26910
|
+
return `${tenantId}:${pluginType}`;
|
|
26911
|
+
}
|
|
26912
|
+
/**
|
|
26913
|
+
* Current time as an ISO string from the injected clock.
|
|
26914
|
+
*
|
|
26915
|
+
* @returns ISO-8601 timestamp.
|
|
26916
|
+
*/
|
|
26917
|
+
nowIso() {
|
|
26918
|
+
return new Date(this.now()).toISOString();
|
|
26919
|
+
}
|
|
26920
|
+
};
|
|
26921
|
+
function errorMessage4(error) {
|
|
26922
|
+
return error instanceof Error ? error.message : String(error);
|
|
26923
|
+
}
|
|
26924
|
+
|
|
26925
|
+
// src/services/plugins/PluginDeploymentRuntimeStartup.ts
|
|
26926
|
+
var DEFAULT_PLUGIN_WATCHDOG_INTERVAL_MS = 3e4;
|
|
26927
|
+
var DEFAULT_PLUGIN_RECONCILE_DEADLINE_MS = 3e4;
|
|
26928
|
+
var DEFAULT_PLUGIN_MAX_PER_TENANT = 10;
|
|
26929
|
+
var DEFAULT_PLUGIN_MAX_GLOBAL = 100;
|
|
26930
|
+
var PLUGIN_DEPLOYMENT_RUNTIME_FLAG = "AXIOM_PLUGIN_DEPLOYMENT_RUNTIME";
|
|
26931
|
+
var PLUGIN_DEPLOYMENT_MAX_PER_TENANT_ENV = "PLUGIN_DEPLOYMENT_MAX_PER_TENANT";
|
|
26932
|
+
var PLUGIN_DEPLOYMENT_MAX_GLOBAL_ENV = "PLUGIN_DEPLOYMENT_MAX_GLOBAL";
|
|
26933
|
+
function isPluginDeploymentRuntimeEnabled(env = process.env) {
|
|
26934
|
+
const flag = env[PLUGIN_DEPLOYMENT_RUNTIME_FLAG];
|
|
26935
|
+
if (flag === "false" || flag === "0") return false;
|
|
26936
|
+
if (flag === "true" || flag === "1") return true;
|
|
26937
|
+
return Boolean(env.MICROSANDBOX_SERVICE_URL);
|
|
26938
|
+
}
|
|
26939
|
+
function readPluginDeploymentLimits(env = process.env) {
|
|
26940
|
+
return {
|
|
26941
|
+
maxPerTenant: parsePositiveInt(
|
|
26942
|
+
env[PLUGIN_DEPLOYMENT_MAX_PER_TENANT_ENV],
|
|
26943
|
+
DEFAULT_PLUGIN_MAX_PER_TENANT
|
|
26944
|
+
),
|
|
26945
|
+
maxGlobal: parsePositiveInt(
|
|
26946
|
+
env[PLUGIN_DEPLOYMENT_MAX_GLOBAL_ENV],
|
|
26947
|
+
DEFAULT_PLUGIN_MAX_GLOBAL
|
|
26948
|
+
)
|
|
26949
|
+
};
|
|
26950
|
+
}
|
|
26951
|
+
function startPluginDeploymentRuntime(options) {
|
|
26952
|
+
const { lease, holder, logger: logger7 } = options;
|
|
26953
|
+
const forcedOn = process.env[PLUGIN_DEPLOYMENT_RUNTIME_FLAG] === "true" || process.env[PLUGIN_DEPLOYMENT_RUNTIME_FLAG] === "1";
|
|
26954
|
+
const effectiveServiceBaseUrl = options.wiring.serviceBaseUrl ?? process.env.MICROSANDBOX_SERVICE_URL;
|
|
26955
|
+
if (forcedOn && !effectiveServiceBaseUrl) {
|
|
26956
|
+
logger7.warn(
|
|
26957
|
+
`${PLUGIN_DEPLOYMENT_RUNTIME_FLAG} is enabled but MICROSANDBOX_SERVICE_URL is empty; skipping plugin deployment runtime`,
|
|
26958
|
+
{ holder }
|
|
26959
|
+
);
|
|
26960
|
+
return { reconcile: Promise.resolve(), dispose: async () => void 0 };
|
|
26961
|
+
}
|
|
26962
|
+
const register = options.register ?? ((service3) => setPluginDeploymentService(service3));
|
|
26963
|
+
const setIntervalFn = options.setIntervalFn ?? setInterval;
|
|
26964
|
+
const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
|
|
26965
|
+
const watchdogIntervalMs = options.watchdogIntervalMs ?? DEFAULT_PLUGIN_WATCHDOG_INTERVAL_MS;
|
|
26966
|
+
const reconcileDeadlineMs = options.reconcileDeadlineMs ?? DEFAULT_PLUGIN_RECONCILE_DEADLINE_MS;
|
|
26967
|
+
const createService = options.createService ?? ((deps2) => new PluginDeploymentService(deps2));
|
|
26968
|
+
const deps = {
|
|
26969
|
+
...options.wiring,
|
|
26970
|
+
lease,
|
|
26971
|
+
replicaId: holder
|
|
26972
|
+
};
|
|
26973
|
+
const service2 = createService(deps);
|
|
26974
|
+
register(service2);
|
|
26975
|
+
logger7.info("plugin deployment runtime started", { holder });
|
|
26976
|
+
const reconcile = runStartupReconcile(service2, reconcileDeadlineMs, logger7, holder);
|
|
26977
|
+
let held = false;
|
|
26978
|
+
let inFlight = false;
|
|
26979
|
+
const tick = async () => {
|
|
26980
|
+
if (inFlight) return;
|
|
26981
|
+
inFlight = true;
|
|
26982
|
+
try {
|
|
26983
|
+
if (held) {
|
|
26984
|
+
held = await renewLease(lease, holder, logger7);
|
|
26985
|
+
if (!held) {
|
|
26986
|
+
logger7.warn("plugin deployment lease lost; watchdog paused", { holder });
|
|
26987
|
+
}
|
|
26988
|
+
} else {
|
|
26989
|
+
held = await acquireLease(lease, holder, logger7);
|
|
26990
|
+
if (held) {
|
|
26991
|
+
logger7.info("plugin deployment lease acquired", { holder });
|
|
26992
|
+
}
|
|
26993
|
+
}
|
|
26994
|
+
if (!held) return;
|
|
26995
|
+
await runWatchdog(service2, holder, logger7);
|
|
26996
|
+
} finally {
|
|
26997
|
+
inFlight = false;
|
|
26998
|
+
}
|
|
26999
|
+
};
|
|
27000
|
+
const interval = setIntervalFn(() => {
|
|
27001
|
+
void tick();
|
|
27002
|
+
}, watchdogIntervalMs);
|
|
27003
|
+
return {
|
|
27004
|
+
reconcile,
|
|
27005
|
+
async dispose() {
|
|
27006
|
+
clearIntervalFn(interval);
|
|
27007
|
+
if (held) {
|
|
27008
|
+
try {
|
|
27009
|
+
await lease.release(PLUGIN_DEPLOYMENT_LEASE_KEY, holder);
|
|
27010
|
+
} catch (error) {
|
|
27011
|
+
logger7.warn("plugin deployment lease release failed", {
|
|
27012
|
+
holder,
|
|
27013
|
+
error: errorMessage5(error)
|
|
27014
|
+
});
|
|
27015
|
+
}
|
|
27016
|
+
held = false;
|
|
27017
|
+
}
|
|
27018
|
+
register(void 0);
|
|
27019
|
+
logger7.info("plugin deployment runtime stopped", { holder });
|
|
27020
|
+
}
|
|
27021
|
+
};
|
|
27022
|
+
}
|
|
27023
|
+
async function runStartupReconcile(service2, deadlineMs, logger7, holder) {
|
|
27024
|
+
try {
|
|
27025
|
+
await withDeadline(service2.startupReconcile(), deadlineMs);
|
|
27026
|
+
logger7.info("plugin deployment startup reconcile complete", { holder });
|
|
27027
|
+
} catch (error) {
|
|
27028
|
+
logger7.warn("plugin deployment startup reconcile failed", {
|
|
27029
|
+
holder,
|
|
27030
|
+
error: errorMessage5(error)
|
|
27031
|
+
});
|
|
27032
|
+
}
|
|
27033
|
+
}
|
|
27034
|
+
async function withDeadline(work, deadlineMs) {
|
|
27035
|
+
const settled = work.then(() => void 0);
|
|
27036
|
+
if (!Number.isFinite(deadlineMs) || deadlineMs <= 0) {
|
|
27037
|
+
await settled;
|
|
27038
|
+
return;
|
|
27039
|
+
}
|
|
27040
|
+
let timer;
|
|
27041
|
+
const deadline = new Promise((resolve3) => {
|
|
27042
|
+
timer = setTimeout(resolve3, deadlineMs);
|
|
27043
|
+
unrefTimer(timer);
|
|
27044
|
+
});
|
|
27045
|
+
try {
|
|
27046
|
+
await Promise.race([settled, deadline]);
|
|
27047
|
+
} finally {
|
|
27048
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
27049
|
+
}
|
|
27050
|
+
}
|
|
27051
|
+
async function acquireLease(lease, holder, logger7) {
|
|
27052
|
+
try {
|
|
27053
|
+
return await lease.tryAcquire(
|
|
27054
|
+
PLUGIN_DEPLOYMENT_LEASE_KEY,
|
|
27055
|
+
holder,
|
|
27056
|
+
PLUGIN_DEPLOYMENT_LEASE_TTL_MS
|
|
27057
|
+
);
|
|
27058
|
+
} catch (error) {
|
|
27059
|
+
logger7.warn("plugin deployment lease acquire failed", {
|
|
27060
|
+
holder,
|
|
27061
|
+
error: errorMessage5(error)
|
|
27062
|
+
});
|
|
27063
|
+
return false;
|
|
27064
|
+
}
|
|
27065
|
+
}
|
|
27066
|
+
async function renewLease(lease, holder, logger7) {
|
|
27067
|
+
try {
|
|
27068
|
+
return await lease.renew(
|
|
27069
|
+
PLUGIN_DEPLOYMENT_LEASE_KEY,
|
|
27070
|
+
holder,
|
|
27071
|
+
PLUGIN_DEPLOYMENT_LEASE_TTL_MS
|
|
27072
|
+
);
|
|
27073
|
+
} catch (error) {
|
|
27074
|
+
logger7.warn("plugin deployment lease renew failed", {
|
|
27075
|
+
holder,
|
|
27076
|
+
error: errorMessage5(error)
|
|
27077
|
+
});
|
|
27078
|
+
return false;
|
|
27079
|
+
}
|
|
27080
|
+
}
|
|
27081
|
+
async function runWatchdog(service2, holder, logger7) {
|
|
27082
|
+
try {
|
|
27083
|
+
await service2.healthWatchdog();
|
|
27084
|
+
} catch (error) {
|
|
27085
|
+
logger7.warn("plugin deployment health watchdog failed", {
|
|
27086
|
+
holder,
|
|
27087
|
+
error: errorMessage5(error)
|
|
27088
|
+
});
|
|
27089
|
+
}
|
|
27090
|
+
}
|
|
27091
|
+
function unrefTimer(timer) {
|
|
27092
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) {
|
|
27093
|
+
timer.unref?.();
|
|
27094
|
+
}
|
|
27095
|
+
}
|
|
27096
|
+
function parsePositiveInt(value, fallback) {
|
|
27097
|
+
if (value === void 0 || value.trim() === "") return fallback;
|
|
27098
|
+
const parsed = Number(value);
|
|
27099
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
27100
|
+
return Math.floor(parsed);
|
|
27101
|
+
}
|
|
27102
|
+
function errorMessage5(error) {
|
|
27103
|
+
return error instanceof Error ? error.message : String(error);
|
|
27104
|
+
}
|
|
27105
|
+
|
|
25787
27106
|
// src/services/multiReplicaReadiness.ts
|
|
25788
27107
|
import { getStoreLattice as getStoreLattice28 } from "@axiom-lattice/core";
|
|
25789
|
-
var SHARED_STORE_KINDS = [
|
|
27108
|
+
var SHARED_STORE_KINDS = [
|
|
27109
|
+
"lease",
|
|
27110
|
+
"outbox",
|
|
27111
|
+
"threadMessageQueue",
|
|
27112
|
+
"task",
|
|
27113
|
+
"taskWorkItem",
|
|
27114
|
+
"channelInstallation"
|
|
27115
|
+
];
|
|
25790
27116
|
function storeClassName(kind) {
|
|
25791
27117
|
try {
|
|
25792
27118
|
return getStoreLattice28("default", kind).store.constructor?.name;
|
|
@@ -26058,6 +27384,7 @@ app.setErrorHandler((error, request, reply) => {
|
|
|
26058
27384
|
error: error.message || "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF"
|
|
26059
27385
|
});
|
|
26060
27386
|
});
|
|
27387
|
+
var shutdownHandlersInstalled = false;
|
|
26061
27388
|
function getConfiguredSandboxProvider() {
|
|
26062
27389
|
const sandboxProviderType = process.env.SANDBOX_PROVIDER_TYPE || "microsandbox-remote";
|
|
26063
27390
|
return createSandboxProvider({
|
|
@@ -26191,18 +27518,18 @@ var start = async (config) => {
|
|
|
26191
27518
|
projectRoomRealtimeRuntime
|
|
26192
27519
|
});
|
|
26193
27520
|
try {
|
|
26194
|
-
const { A2AAuthService, parseEnvKeys } = await import("./A2AAuthService-7UGQ7GMW.mjs");
|
|
27521
|
+
const { A2AAuthService: A2AAuthService2, parseEnvKeys: parseEnvKeys2 } = await import("./A2AAuthService-7UGQ7GMW.mjs");
|
|
26195
27522
|
const { registerA2AStandardRoutes } = await import("./a2a-standard-PBYPNHOA.mjs");
|
|
26196
|
-
const a2a = await import("./a2a-
|
|
27523
|
+
const a2a = await import("./a2a-BHBLACJV.mjs");
|
|
26197
27524
|
const a2aKeyStore = getStoreLattice29("default", "a2aApiKey").store;
|
|
26198
27525
|
const assistantStore = getStoreLattice29("default", "assistant").store;
|
|
26199
27526
|
const projectStore = getStoreLattice29("default", "project").store;
|
|
26200
27527
|
const taskStore = getStoreLattice29("default", "task").store;
|
|
26201
|
-
const authService = new
|
|
27528
|
+
const authService = new A2AAuthService2({
|
|
26202
27529
|
keyStore: a2aKeyStore,
|
|
26203
27530
|
assistantStore,
|
|
26204
27531
|
projectStore,
|
|
26205
|
-
envKeyMap:
|
|
27532
|
+
envKeyMap: parseEnvKeys2(process.env.A2A_API_KEYS || ""),
|
|
26206
27533
|
allowUnauthenticated: process.env.A2A_ALLOW_UNAUTHENTICATED === "true"
|
|
26207
27534
|
});
|
|
26208
27535
|
await authService.refresh();
|
|
@@ -26227,7 +27554,7 @@ var start = async (config) => {
|
|
|
26227
27554
|
const { registerOpenCatalogRoutes } = await import("./open-catalog-SZQ435AY.mjs");
|
|
26228
27555
|
const { registerOpenFilesRoutes } = await import("./open-files-X3HXVEEM.mjs");
|
|
26229
27556
|
const { registerOpenAuditRoutes } = await import("./open-audit-UJ3NXCGW.mjs");
|
|
26230
|
-
const { registerOpenExposureRoutes } = await import("./open-exposure-
|
|
27557
|
+
const { registerOpenExposureRoutes } = await import("./open-exposure-27ESFWTB.mjs");
|
|
26231
27558
|
const { openLattice } = await import("./openLattice-Q6M5S6CY.mjs");
|
|
26232
27559
|
const { createKbProvider } = await import("./kbProvider-2D6DZ34N.mjs");
|
|
26233
27560
|
const { createOpenSandboxProvider } = await import("./sandboxProvider-DUYWHYJN.mjs");
|
|
@@ -26275,6 +27602,50 @@ var start = async (config) => {
|
|
|
26275
27602
|
sandboxLatticeManager2.registerLattice("default", getConfiguredSandboxProvider());
|
|
26276
27603
|
logger6.info("Registered sandbox manager from env configuration");
|
|
26277
27604
|
}
|
|
27605
|
+
if (isPluginDeploymentRuntimeEnabled()) {
|
|
27606
|
+
try {
|
|
27607
|
+
const pluginDeploymentStore = getStoreLattice29("default", "pluginDeployment").store;
|
|
27608
|
+
const tenantStore = getStoreLattice29("default", "tenant").store;
|
|
27609
|
+
const leaseStore2 = getStoreLattice29("default", "lease").store;
|
|
27610
|
+
const listAllDeployments = async () => {
|
|
27611
|
+
const tenants = await tenantStore.getAllTenants();
|
|
27612
|
+
const lists = await Promise.all(
|
|
27613
|
+
tenants.map(
|
|
27614
|
+
(tenant2) => pluginDeploymentStore.listByTenant(tenant2.id).catch(() => [])
|
|
27615
|
+
)
|
|
27616
|
+
);
|
|
27617
|
+
return lists.flat();
|
|
27618
|
+
};
|
|
27619
|
+
const pluginDeploymentRuntime = startPluginDeploymentRuntime({
|
|
27620
|
+
wiring: {
|
|
27621
|
+
store: pluginDeploymentStore,
|
|
27622
|
+
sandboxManager: getSandBoxManager4(),
|
|
27623
|
+
catalog: getSandboxPluginCatalog3(),
|
|
27624
|
+
installPluginHost,
|
|
27625
|
+
limits: readPluginDeploymentLimits(),
|
|
27626
|
+
serviceKey: process.env.MICROSANDBOX_API_KEY,
|
|
27627
|
+
listAllDeployments,
|
|
27628
|
+
// The service logs fields-first; the gateway logger is message-first.
|
|
27629
|
+
logger: {
|
|
27630
|
+
info: (fields, message) => logger6.info(message ?? "plugin deployment", fields),
|
|
27631
|
+
warn: (fields, message) => logger6.warn(message ?? "plugin deployment", fields),
|
|
27632
|
+
error: (fields, message) => logger6.error(message ?? "plugin deployment", fields)
|
|
27633
|
+
}
|
|
27634
|
+
},
|
|
27635
|
+
lease: leaseStore2,
|
|
27636
|
+
holder: getReplicaId7(),
|
|
27637
|
+
logger: logger6
|
|
27638
|
+
});
|
|
27639
|
+
app.addHook("onClose", async () => {
|
|
27640
|
+
await pluginDeploymentRuntime.dispose();
|
|
27641
|
+
});
|
|
27642
|
+
logger6.info("Plugin deployment runtime started");
|
|
27643
|
+
} catch (err) {
|
|
27644
|
+
logger6.warn("Plugin deployment runtime unavailable", {
|
|
27645
|
+
error: err instanceof Error ? err.message : String(err)
|
|
27646
|
+
});
|
|
27647
|
+
}
|
|
27648
|
+
}
|
|
26278
27649
|
try {
|
|
26279
27650
|
const { ResourceController } = await import("./resources-2L3KSUUU.mjs");
|
|
26280
27651
|
const sharedResourceStore = getStoreLattice29("default", "sharedResource").store;
|
|
@@ -26293,12 +27664,21 @@ var start = async (config) => {
|
|
|
26293
27664
|
});
|
|
26294
27665
|
}
|
|
26295
27666
|
if (channelDeps && process.env.CHANNELS_ENABLED !== "false") {
|
|
26296
|
-
const { connectAllChannels } = await import("@axiom-lattice/core");
|
|
27667
|
+
const { connectAllChannels, disconnectAllChannels, ChannelConnectionLeader } = await import("@axiom-lattice/core");
|
|
26297
27668
|
try {
|
|
26298
|
-
|
|
26299
|
-
(
|
|
26300
|
-
|
|
26301
|
-
|
|
27669
|
+
const channelLeader = new ChannelConnectionLeader({
|
|
27670
|
+
leaseStore: getStoreLattice29("default", "lease").store,
|
|
27671
|
+
owner: getReplicaId7(),
|
|
27672
|
+
connect: () => connectAllChannels((channel) => adapterRegistry.get(channel), {
|
|
27673
|
+
deps: { router: channelDeps.router }
|
|
27674
|
+
}),
|
|
27675
|
+
disconnect: () => disconnectAllChannels((channel) => adapterRegistry.get(channel)),
|
|
27676
|
+
log: { warn: (message, context) => logger6.warn(message, context) }
|
|
27677
|
+
});
|
|
27678
|
+
app.addHook("onClose", async () => {
|
|
27679
|
+
await channelLeader.stop();
|
|
27680
|
+
});
|
|
27681
|
+
channelLeader.start();
|
|
26302
27682
|
} catch (err) {
|
|
26303
27683
|
logger6.error("Failed to start channel connections", {
|
|
26304
27684
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -26310,20 +27690,20 @@ var start = async (config) => {
|
|
|
26310
27690
|
const outboxStore = getStoreLattice29("default", "outbox").store;
|
|
26311
27691
|
taskLifecycleNotifier3.setOutbox(outboxStore);
|
|
26312
27692
|
projectTaskAdmissionNotifier2.setOutbox(outboxStore);
|
|
26313
|
-
configurePluginCatalogSync({ outbox: outboxStore, owner:
|
|
27693
|
+
configurePluginCatalogSync({ outbox: outboxStore, owner: getReplicaId7() });
|
|
26314
27694
|
bridgeAssistantEvents(
|
|
26315
27695
|
(event, cb) => eventBus4.subscribe(event, cb),
|
|
26316
27696
|
outboxStore,
|
|
26317
|
-
|
|
27697
|
+
getReplicaId7()
|
|
26318
27698
|
);
|
|
26319
27699
|
if (projectRoomRealtimeRuntime) {
|
|
26320
27700
|
projectRoomRealtimeRuntime.broker.setBroadcastHook((scope, serialized, sourceKey) => {
|
|
26321
|
-
void publishRoomEvent(outboxStore,
|
|
27701
|
+
void publishRoomEvent(outboxStore, getReplicaId7(), scope, serialized, sourceKey);
|
|
26322
27702
|
});
|
|
26323
27703
|
}
|
|
26324
27704
|
const outboxPoller = new OutboxPoller({
|
|
26325
27705
|
store: outboxStore,
|
|
26326
|
-
owner:
|
|
27706
|
+
owner: getReplicaId7(),
|
|
26327
27707
|
handlers: {
|
|
26328
27708
|
"task-lifecycle": (payload) => taskLifecycleNotifier3.deliverFromOutbox(payload),
|
|
26329
27709
|
"project-task-admission": (payload) => projectTaskAdmissionNotifier2.deliverFromOutbox(payload),
|
|
@@ -26331,6 +27711,7 @@ var start = async (config) => {
|
|
|
26331
27711
|
if (projectRoomRealtimeRuntime) ingestRoomEvent(payload, projectRoomRealtimeRuntime.broker);
|
|
26332
27712
|
},
|
|
26333
27713
|
"assistant-changed": (payload) => handleAssistantChanged(payload, (tenantId) => agentLatticeManager5.initializeStoredAssistantsForTenant(tenantId)),
|
|
27714
|
+
[A2A_KEY_CHANGED_TOPIC]: (payload) => handleA2AKeyChanged(payload, () => refreshStoreKeyMap()),
|
|
26334
27715
|
[MCP_CONFIG_CHANGED_TOPIC]: (payload) => reconcileMcpConfigChange(payload, {
|
|
26335
27716
|
store: getStoreLattice29("default", "mcp").store,
|
|
26336
27717
|
manager: mcpManager3,
|
|
@@ -26339,6 +27720,11 @@ var start = async (config) => {
|
|
|
26339
27720
|
[PLUGIN_CATALOG_CHANGED_TOPIC]: async (payload) => {
|
|
26340
27721
|
const tenantId = payload?.tenantId;
|
|
26341
27722
|
if (typeof tenantId === "string" && tenantId.length > 0) getSandboxPluginCatalog3().invalidate(tenantId);
|
|
27723
|
+
},
|
|
27724
|
+
[MODEL_PROVIDER_CHANGED_TOPIC]: async (payload) => {
|
|
27725
|
+
const controller = routes.modelProviderController;
|
|
27726
|
+
if (!controller) return;
|
|
27727
|
+
await handleModelProviderChanged(payload, (tenantId, providerId2) => controller.reconcileProvider(tenantId, providerId2));
|
|
26342
27728
|
}
|
|
26343
27729
|
},
|
|
26344
27730
|
log: logger6
|
|
@@ -26398,17 +27784,41 @@ var start = async (config) => {
|
|
|
26398
27784
|
log: logger6
|
|
26399
27785
|
});
|
|
26400
27786
|
outboxRetention.start();
|
|
27787
|
+
let agentTaskConsumer;
|
|
26401
27788
|
const queueServiceConfig = config?.queueServiceConfig;
|
|
26402
27789
|
if (queueServiceConfig) {
|
|
26403
27790
|
setQueueServiceType(queueServiceConfig.type);
|
|
26404
27791
|
if (queueServiceConfig.defaultStartPollingQueue) {
|
|
26405
|
-
|
|
27792
|
+
agentTaskConsumer = new AgentTaskConsumer(target_port);
|
|
26406
27793
|
agentTaskConsumer.startPollingQueue();
|
|
26407
27794
|
}
|
|
26408
27795
|
}
|
|
26409
27796
|
restoreMcpConnections().catch((error) => {
|
|
26410
27797
|
logger6.error("MCP connection restoration failed", { error });
|
|
26411
27798
|
});
|
|
27799
|
+
if (config?.gracefulShutdown !== false && !shutdownHandlersInstalled) {
|
|
27800
|
+
shutdownHandlersInstalled = true;
|
|
27801
|
+
const gracefulShutdown = createGracefulShutdown({
|
|
27802
|
+
timeoutMs: config?.gracefulShutdownTimeoutMs ?? 1e4,
|
|
27803
|
+
log: {
|
|
27804
|
+
info: (message, context) => logger6.info(message, context),
|
|
27805
|
+
warn: (message, context) => logger6.warn(message, context)
|
|
27806
|
+
},
|
|
27807
|
+
steps: [
|
|
27808
|
+
async () => {
|
|
27809
|
+
await Promise.all(
|
|
27810
|
+
agentInstanceManager9.getAllAgents().map((agent) => agent.abort().catch(() => void 0))
|
|
27811
|
+
);
|
|
27812
|
+
},
|
|
27813
|
+
() => agentTaskConsumer?.stopPollingQueue(),
|
|
27814
|
+
() => outboxPoller.stop(),
|
|
27815
|
+
() => outboxRetention.stop(),
|
|
27816
|
+
() => app.close()
|
|
27817
|
+
]
|
|
27818
|
+
});
|
|
27819
|
+
process.once("SIGTERM", () => void gracefulShutdown("SIGTERM"));
|
|
27820
|
+
process.once("SIGINT", () => void gracefulShutdown("SIGINT"));
|
|
27821
|
+
}
|
|
26412
27822
|
} catch (err) {
|
|
26413
27823
|
logger6.error("Server start failed", { error: err });
|
|
26414
27824
|
process.exit(1);
|
|
@@ -26422,13 +27832,22 @@ var LatticeGateway = {
|
|
|
26422
27832
|
AgentTaskConsumer
|
|
26423
27833
|
};
|
|
26424
27834
|
export {
|
|
27835
|
+
A2AAuthService,
|
|
27836
|
+
A2A_KEY_CHANGED_TOPIC,
|
|
26425
27837
|
LatticeGateway,
|
|
27838
|
+
MODEL_PROVIDER_CHANGED_TOPIC,
|
|
27839
|
+
ModelProviderService,
|
|
26426
27840
|
ProjectRoomEventBroker,
|
|
26427
27841
|
ROOM_EVENT_TOPIC,
|
|
26428
27842
|
createOrphanRecoveryRuntime,
|
|
26429
27843
|
createRecoveryCallerDeps,
|
|
26430
27844
|
createTaskRunLiveness,
|
|
27845
|
+
handleA2AKeyChanged,
|
|
27846
|
+
handleModelProviderChanged,
|
|
26431
27847
|
ingestRoomEvent,
|
|
27848
|
+
parseEnvKeys,
|
|
27849
|
+
publishA2AKeyChanged,
|
|
27850
|
+
publishModelProviderChanged,
|
|
26432
27851
|
publishRoomEvent,
|
|
26433
27852
|
recoverTask,
|
|
26434
27853
|
recoverTopCaller,
|