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