@axiom-lattice/core 6.3.3 → 6.4.1

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/index.js CHANGED
@@ -1621,6 +1621,7 @@ __export(index_exports, {
1621
1621
  CapabilityBundleValidationError: () => CapabilityBundleValidationError,
1622
1622
  CapabilityToolNameConflictError: () => CapabilityToolNameConflictError,
1623
1623
  CapabilityToolRegistry: () => CapabilityToolRegistry,
1624
+ ChannelConnectionLeader: () => ChannelConnectionLeader,
1624
1625
  ChunkBuffer: () => ChunkBuffer,
1625
1626
  ChunkBufferLatticeManager: () => ChunkBufferLatticeManager,
1626
1627
  CollectionLatticeManager: () => CollectionLatticeManager,
@@ -1632,7 +1633,9 @@ __export(index_exports, {
1632
1633
  DEFAULT_CALIBRATION_PROBES: () => DEFAULT_CALIBRATION_PROBES,
1633
1634
  DEFAULT_CONNECT_TIMEOUT_MS: () => DEFAULT_CONNECT_TIMEOUT_MS,
1634
1635
  DEFAULT_DELAY_MS: () => DEFAULT_DELAY_MS,
1636
+ DEFAULT_HANDLER_TIMEOUT_MS: () => DEFAULT_HANDLER_TIMEOUT_MS,
1635
1637
  DEFAULT_MAX_ATTEMPTS: () => DEFAULT_MAX_ATTEMPTS,
1638
+ DEFAULT_RESIDENT_INVOKE_TIMEOUT_MS: () => DEFAULT_RESIDENT_INVOKE_TIMEOUT_MS,
1636
1639
  DaytonaInstance: () => DaytonaInstance,
1637
1640
  DaytonaProvider: () => DaytonaProvider,
1638
1641
  DefaultScheduleClient: () => DefaultScheduleClient,
@@ -1650,6 +1653,7 @@ __export(index_exports, {
1650
1653
  HEALTH_CHECK_INTERVAL_MS: () => HEALTH_CHECK_INTERVAL_MS,
1651
1654
  HEALTH_CHECK_TIMEOUT_MS: () => HEALTH_CHECK_TIMEOUT_MS,
1652
1655
  HumanMessage: () => import_messages9.HumanMessage,
1656
+ INLINE_INPUT_LIMIT_BYTES: () => INLINE_INPUT_LIMIT_BYTES2,
1653
1657
  IdRemapper: () => IdRemapper,
1654
1658
  InMemoryA2AApiKeyStore: () => InMemoryA2AApiKeyStore,
1655
1659
  InMemoryAgentWebAppStore: () => InMemoryAgentWebAppStore,
@@ -1690,10 +1694,12 @@ __export(index_exports, {
1690
1694
  LocalSandboxProvider: () => LocalSandboxProvider,
1691
1695
  LoggerLatticeManager: () => LoggerLatticeManager,
1692
1696
  MAX_BACKOFF_MS: () => MAX_BACKOFF_MS,
1697
+ MAX_HANDLER_TIMEOUT_MS: () => MAX_HANDLER_TIMEOUT_MS,
1693
1698
  MAX_LINE_LENGTH: () => MAX_LINE_LENGTH,
1694
1699
  MAX_PACK_ENTRIES: () => MAX_PACK_ENTRIES,
1695
1700
  MAX_PACK_FILE_BYTES: () => MAX_PACK_FILE_BYTES,
1696
1701
  MAX_PACK_TOTAL_BYTES: () => MAX_PACK_TOTAL_BYTES,
1702
+ MIN_HANDLER_TIMEOUT_MS: () => MIN_HANDLER_TIMEOUT_MS,
1697
1703
  ManagedTaskStartError: () => ManagedTaskStartError,
1698
1704
  McpLatticeManager: () => McpLatticeManager,
1699
1705
  MemoryBackend: () => MemoryBackend,
@@ -1712,8 +1718,12 @@ __export(index_exports, {
1712
1718
  PACK_DIRECTORY_TIMEOUT_SECONDS: () => PACK_DIRECTORY_TIMEOUT_SECONDS,
1713
1719
  PLATFORM_SECRET_ENV_VARS: () => PLATFORM_SECRET_ENV_VARS,
1714
1720
  PLUGIN_CATALOG_CHANGED_TOPIC: () => PLUGIN_CATALOG_CHANGED_TOPIC,
1721
+ PLUGIN_HOST_DIR: () => PLUGIN_HOST_DIR,
1722
+ PLUGIN_HOST_SERVER_SOURCE: () => PLUGIN_HOST_SERVER_SOURCE,
1723
+ PLUGIN_HOST_VERSION: () => PLUGIN_HOST_VERSION,
1715
1724
  PersonalAssistantConfig: () => PersonalAssistantConfig,
1716
1725
  PinoLoggerClient: () => PinoLoggerClient,
1726
+ PluginDeploymentRuntimeRegistry: () => PluginDeploymentRuntimeRegistry,
1717
1727
  PluginRegistry: () => PluginRegistry,
1718
1728
  PostgresDatabase: () => PostgresDatabase,
1719
1729
  ProjectBotMembershipIdConflictError: () => ProjectBotMembershipIdConflictError,
@@ -1775,7 +1785,9 @@ __export(index_exports, {
1775
1785
  appendUiFence: () => appendUiFence,
1776
1786
  assertSafeUpstreamUrl: () => assertSafeUpstreamUrl,
1777
1787
  buildBundleFromDist: () => buildBundleFromDist,
1788
+ buildConnectionPayload: () => buildConnectionPayload2,
1778
1789
  buildGrepResultsDict: () => buildGrepResultsDict,
1790
+ buildHandlerCommand: () => buildHandlerCommand,
1779
1791
  buildInput: () => buildInput,
1780
1792
  buildNamedVolumeName: () => buildNamedVolumeName,
1781
1793
  buildRemotePackDirectoryCommand: () => buildRemotePackDirectoryCommand,
@@ -1843,9 +1855,11 @@ __export(index_exports, {
1843
1855
  decrypt: () => decrypt,
1844
1856
  decryptConfigFields: () => decryptConfigFields,
1845
1857
  describeCronExpression: () => describeCronExpression,
1858
+ disconnectAllChannels: () => disconnectAllChannels,
1846
1859
  documentLearningPlugin: () => documentLearningPlugin,
1847
1860
  documentParserPlugin: () => documentParserPlugin,
1848
1861
  embeddingsLatticeManager: () => embeddingsLatticeManager,
1862
+ encodeInput: () => encodeInput,
1849
1863
  encrypt: () => encrypt,
1850
1864
  encryptConfigFields: () => encryptConfigFields,
1851
1865
  ensureBuiltinAgentsForTenant: () => ensureBuiltinAgentsForTenant,
@@ -1889,6 +1903,7 @@ __export(index_exports, {
1889
1903
  getModelLattice: () => getModelLattice,
1890
1904
  getNextCronTime: () => getNextCronTime,
1891
1905
  getOrCreateCollectionVectorStore: () => getOrCreateCollectionVectorStore,
1906
+ getPluginDeploymentService: () => getPluginDeploymentService,
1892
1907
  getPluginSkillManifestVersion: () => getPluginSkillManifestVersion,
1893
1908
  getQueueLattice: () => getQueueLattice,
1894
1909
  getReplicaId: () => getReplicaId,
@@ -1914,9 +1929,12 @@ __export(index_exports, {
1914
1929
  globSearchFiles: () => globSearchFiles,
1915
1930
  grepMatchesFromFiles: () => grepMatchesFromFiles,
1916
1931
  grepSearchFiles: () => grepSearchFiles,
1932
+ handlerResultText: () => handlerResultText,
1917
1933
  hasChunkBuffer: () => hasChunkBuffer,
1918
1934
  hasDelegatedWorkerToken: () => hasDelegatedWorkerToken,
1919
1935
  hasRuntimeCapabilityPolicy: () => hasRuntimeCapabilityPolicy,
1936
+ installPluginHost: () => installPluginHost,
1937
+ invokeResident: () => invokeResident,
1920
1938
  invokeWithRetry: () => invokeWithRetry,
1921
1939
  isBuiltInSkill: () => isBuiltInSkill,
1922
1940
  isDirectRoom: () => isDirectRoom,
@@ -1944,6 +1962,8 @@ __export(index_exports, {
1944
1962
  parallelLimit: () => parallelLimit,
1945
1963
  parseCronExpression: () => parseCronExpression,
1946
1964
  parseDirectRoomType: () => parseDirectRoomType,
1965
+ parseHandlerEnvelope: () => parseHandlerEnvelope,
1966
+ parseHostArgs: () => parseHostArgs,
1947
1967
  parseJudgeVerdict: () => parseJudgeVerdict,
1948
1968
  parsePackSize: () => parsePackSize,
1949
1969
  parseQueuedExecutionMode: () => import_protocols2.parseQueuedExecutionMode,
@@ -1952,6 +1972,7 @@ __export(index_exports, {
1952
1972
  parseTrustedRunContext: () => import_protocols2.parseTrustedRunContext,
1953
1973
  parseYaml: () => parseYaml,
1954
1974
  performStringReplacement: () => performStringReplacement,
1975
+ pluginHostContentHash: () => pluginHostContentHash,
1955
1976
  postProcessTaskMutationResponse: () => postProcessTaskMutationResponse,
1956
1977
  prepareTaskDispatch: () => prepareTaskDispatch,
1957
1978
  projectMutationMutex: () => projectMutationMutex,
@@ -1990,6 +2011,7 @@ __export(index_exports, {
1990
2011
  renderTemplate: () => renderTemplate,
1991
2012
  resetReplicaIdForTests: () => resetReplicaIdForTests,
1992
2013
  resolveEnabledPluginSkill: () => resolveEnabledPluginSkill,
2014
+ resolveHandlerTimeoutMs: () => resolveHandlerTimeoutMs,
1993
2015
  resolveJudgeModelKey: () => resolveJudgeModelKey,
1994
2016
  resolvePath: () => resolvePath,
1995
2017
  resolvePluginConnections: () => resolvePluginConnections,
@@ -2009,6 +2031,7 @@ __export(index_exports, {
2009
2031
  setBindingRegistry: () => setBindingRegistry,
2010
2032
  setEvalRunService: () => setEvalRunService,
2011
2033
  setMenuRegistry: () => setMenuRegistry,
2034
+ setPluginDeploymentService: () => setPluginDeploymentService,
2012
2035
  setSandboxPluginCatalog: () => setSandboxPluginCatalog,
2013
2036
  skillLatticeManager: () => skillLatticeManager,
2014
2037
  sqlDatabaseManager: () => sqlDatabaseManager,
@@ -3706,6 +3729,9 @@ var InMemoryProjectStore = class {
3706
3729
  workspaceId,
3707
3730
  name: data.name,
3708
3731
  description: data.description,
3732
+ // Persist `kind` like the Local/PostgreSQL stores so `public`/`personal`/
3733
+ // `training` project semantics survive in in-memory deployments.
3734
+ ...data.kind === void 0 ? {} : { kind: data.kind },
3709
3735
  config: data.config === void 0 && Object.keys(capabilityBundleIds).length === 0 ? void 0 : { ...data.config ?? {}, ...capabilityBundleIds },
3710
3736
  createdAt: existing?.createdAt ?? now,
3711
3737
  updatedAt: now
@@ -5325,6 +5351,93 @@ var InMemoryOutboxStore = class {
5325
5351
  }
5326
5352
  };
5327
5353
 
5354
+ // src/store_lattice/InMemoryPluginDeploymentStore.ts
5355
+ var InMemoryPluginDeploymentStore = class {
5356
+ constructor() {
5357
+ this.entries = /* @__PURE__ */ new Map();
5358
+ }
5359
+ /**
5360
+ * Lists every deployment for a tenant.
5361
+ *
5362
+ * @param tenantId - Tenant whose deployments are listed
5363
+ * @returns Defensive copies of the tenant's deployments
5364
+ */
5365
+ async listByTenant(tenantId2) {
5366
+ return [...this.entries.values()].filter((entry) => entry.tenantId === tenantId2).map((entry) => structuredClone(entry));
5367
+ }
5368
+ /**
5369
+ * Returns the deployment for a tenant + plugin type.
5370
+ *
5371
+ * @param tenantId - Owning tenant
5372
+ * @param pluginType - Plugin type key
5373
+ * @returns Defensive copy of the deployment, or undefined when absent
5374
+ */
5375
+ async get(tenantId2, pluginType) {
5376
+ const entry = this.entries.get(entryKey2(tenantId2, pluginType));
5377
+ return entry ? structuredClone(entry) : void 0;
5378
+ }
5379
+ /**
5380
+ * Inserts or replaces a deployment keyed by tenant + plugin type.
5381
+ *
5382
+ * On conflict the existing `id` and `createdAt` are preserved while every
5383
+ * other field (including `updatedAt`, when supplied) is updated — mirroring
5384
+ * the PostgreSQL store's `ON CONFLICT` semantics.
5385
+ *
5386
+ * @param deployment - Deployment record to persist
5387
+ * @returns Defensive copy of the stored deployment
5388
+ */
5389
+ async upsert(deployment) {
5390
+ const key5 = entryKey2(deployment.tenantId, deployment.pluginType);
5391
+ const existing = this.entries.get(key5);
5392
+ const next = {
5393
+ ...structuredClone(deployment),
5394
+ id: existing?.id ?? deployment.id,
5395
+ createdAt: existing?.createdAt ?? deployment.createdAt,
5396
+ updatedAt: deployment.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
5397
+ };
5398
+ this.entries.set(key5, next);
5399
+ return structuredClone(next);
5400
+ }
5401
+ /**
5402
+ * Applies a partial update to a deployment.
5403
+ *
5404
+ * `patch.createdAt` is ignored (immutable) and `updatedAt` is taken from the
5405
+ * patch when supplied, otherwise set to the current time — matching the
5406
+ * PostgreSQL store.
5407
+ *
5408
+ * @param tenantId - Owning tenant
5409
+ * @param pluginType - Plugin type key
5410
+ * @param patch - Mutable fields to apply
5411
+ * @returns Defensive copy of the updated deployment, or undefined when absent
5412
+ */
5413
+ async updateStatus(tenantId2, pluginType, patch) {
5414
+ const key5 = entryKey2(tenantId2, pluginType);
5415
+ const existing = this.entries.get(key5);
5416
+ if (!existing) return void 0;
5417
+ const next = {
5418
+ ...existing,
5419
+ ...patch,
5420
+ createdAt: existing.createdAt,
5421
+ updatedAt: patch.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
5422
+ };
5423
+ this.entries.set(key5, next);
5424
+ return structuredClone(next);
5425
+ }
5426
+ /**
5427
+ * Deletes a deployment by tenant + plugin type.
5428
+ *
5429
+ * @param tenantId - Owning tenant
5430
+ * @param pluginType - Plugin type key
5431
+ * @returns Whether a record was removed
5432
+ */
5433
+ async delete(tenantId2, pluginType) {
5434
+ return this.entries.delete(entryKey2(tenantId2, pluginType));
5435
+ }
5436
+ };
5437
+ function entryKey2(tenantId2, pluginType) {
5438
+ return JSON.stringify([tenantId2, pluginType]);
5439
+ }
5440
+
5328
5441
  // src/store_lattice/InMemoryUserStore.ts
5329
5442
  var InMemoryUserStore = class {
5330
5443
  constructor() {
@@ -6987,6 +7100,7 @@ var defaultProjectRoomReadStateStore = new InMemoryProjectRoomReadStateStore();
6987
7100
  var defaultUserPushSubscriptionStore = new InMemoryUserPushSubscriptionStore();
6988
7101
  var defaultLeaseStore = new InMemoryLeaseStore();
6989
7102
  var defaultOutboxStore = new InMemoryOutboxStore();
7103
+ var defaultPluginDeploymentStore = new InMemoryPluginDeploymentStore();
6990
7104
  storeLatticeManager.registerLattice("default", "thread", defaultThreadStore);
6991
7105
  storeLatticeManager.registerLattice(
6992
7106
  "default",
@@ -7010,6 +7124,7 @@ storeLatticeManager.registerLattice("default", "projectRoomReadState", defaultPr
7010
7124
  storeLatticeManager.registerLattice("default", "userPushSubscription", defaultUserPushSubscriptionStore);
7011
7125
  storeLatticeManager.registerLattice("default", "lease", defaultLeaseStore);
7012
7126
  storeLatticeManager.registerLattice("default", "outbox", defaultOutboxStore);
7127
+ storeLatticeManager.registerLattice("default", "pluginDeployment", defaultPluginDeploymentStore);
7013
7128
  var defaultUserStore = new InMemoryUserStore();
7014
7129
  var defaultTenantStore = new InMemoryTenantStore();
7015
7130
  var defaultUserTenantLinkStore = new InMemoryUserTenantLinkStore();
@@ -15069,6 +15184,82 @@ var evalPlugin = {
15069
15184
  })
15070
15185
  };
15071
15186
 
15187
+ // src/sandbox_plugin/residentTransport.ts
15188
+ var DEFAULT_RESIDENT_INVOKE_TIMEOUT_MS = 6e4;
15189
+ function isRecord3(value) {
15190
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15191
+ }
15192
+ function isEnvelope(value) {
15193
+ return isRecord3(value) && typeof value.ok === "boolean";
15194
+ }
15195
+ function errorFromBody(value) {
15196
+ if (isRecord3(value) && typeof value.error === "string" && value.error.length > 0) {
15197
+ return value.error;
15198
+ }
15199
+ return void 0;
15200
+ }
15201
+ async function readJson(response) {
15202
+ try {
15203
+ return await response.json();
15204
+ } catch {
15205
+ return void 0;
15206
+ }
15207
+ }
15208
+ async function invokeResident(args) {
15209
+ const {
15210
+ baseUrl,
15211
+ endpointPath,
15212
+ serviceKey,
15213
+ payload,
15214
+ timeoutMs = DEFAULT_RESIDENT_INVOKE_TIMEOUT_MS
15215
+ } = args;
15216
+ const fetchImpl = args.fetchImpl ?? fetch;
15217
+ const url = `${baseUrl.replace(/\/$/, "")}${endpointPath}/invoke`;
15218
+ const headers = {
15219
+ "Content-Type": "application/json"
15220
+ };
15221
+ if (serviceKey) {
15222
+ headers["Authorization"] = `Bearer ${serviceKey}`;
15223
+ }
15224
+ const controller = new AbortController();
15225
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
15226
+ try {
15227
+ const response = await fetchImpl(url, {
15228
+ method: "POST",
15229
+ headers,
15230
+ body: JSON.stringify({ ...payload, timeoutMs: payload.timeoutMs ?? timeoutMs }),
15231
+ signal: controller.signal
15232
+ });
15233
+ const parsed = await readJson(response);
15234
+ if (!response.ok) {
15235
+ return {
15236
+ ok: false,
15237
+ error: errorFromBody(parsed) ?? `resident service responded with HTTP ${response.status}`,
15238
+ status: response.status
15239
+ };
15240
+ }
15241
+ if (!isEnvelope(parsed)) {
15242
+ return {
15243
+ ok: false,
15244
+ error: "resident service returned an invalid envelope",
15245
+ status: response.status
15246
+ };
15247
+ }
15248
+ if (!parsed.ok) {
15249
+ return {
15250
+ ok: false,
15251
+ error: errorFromBody(parsed) ?? "resident handler reported failure",
15252
+ status: response.status
15253
+ };
15254
+ }
15255
+ return { ok: true, result: parsed.result, status: response.status };
15256
+ } catch (error) {
15257
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
15258
+ } finally {
15259
+ clearTimeout(timer);
15260
+ }
15261
+ }
15262
+
15072
15263
  // src/sandbox_plugin/SandboxPluginExecutor.ts
15073
15264
  var PLUGINS_ROOT = "/root/.agents/plugins";
15074
15265
  var INLINE_INPUT_LIMIT_BYTES = 48 * 1024;
@@ -15081,11 +15272,11 @@ var MAX_MAX_RESULT_BYTES = 1048576;
15081
15272
  function encode(value) {
15082
15273
  return Buffer.from(JSON.stringify(value ?? null), "utf8").toString("base64");
15083
15274
  }
15084
- function isRecord3(value) {
15275
+ function isRecord4(value) {
15085
15276
  return typeof value === "object" && value !== null && !Array.isArray(value);
15086
15277
  }
15087
- function isEnvelope(value) {
15088
- return isRecord3(value) && typeof value.ok === "boolean";
15278
+ function isEnvelope2(value) {
15279
+ return isRecord4(value) && typeof value.ok === "boolean";
15089
15280
  }
15090
15281
  function clamp(value, min, max) {
15091
15282
  return Math.min(Math.max(value, min), max);
@@ -15106,7 +15297,7 @@ function parseEnvelope(stdout) {
15106
15297
  const trimmed = stdout.trim();
15107
15298
  try {
15108
15299
  const parsed = JSON.parse(trimmed || "null");
15109
- if (isEnvelope(parsed)) return parsed;
15300
+ if (isEnvelope2(parsed)) return parsed;
15110
15301
  } catch {
15111
15302
  }
15112
15303
  const lines = stdout.split("\n");
@@ -15115,7 +15306,7 @@ function parseEnvelope(stdout) {
15115
15306
  if (!line) continue;
15116
15307
  try {
15117
15308
  const parsed = JSON.parse(line);
15118
- if (isEnvelope(parsed)) return parsed;
15309
+ if (isEnvelope2(parsed)) return parsed;
15119
15310
  } catch {
15120
15311
  }
15121
15312
  }
@@ -15128,26 +15319,264 @@ function truncate(value, cap) {
15128
15319
  while (end > 0 && (buf[end] & 192) === 128) end -= 1;
15129
15320
  return buf.subarray(0, end).toString("utf8") + "\n...[truncated]";
15130
15321
  }
15322
+ function capResult(result, cap) {
15323
+ const serialized = typeof result === "string" ? result : JSON.stringify(result ?? null);
15324
+ return Buffer.byteLength(serialized, "utf8") <= cap ? result : truncate(serialized, cap);
15325
+ }
15326
+ function handlerResultText(result) {
15327
+ const value = result.result;
15328
+ if (typeof value === "string") return value;
15329
+ if (value === void 0) return (result.stdout ?? "").trim();
15330
+ return JSON.stringify(value);
15331
+ }
15131
15332
  function errorFromStdout(stdout) {
15132
15333
  let text = stdout;
15133
15334
  try {
15134
15335
  const parsed = JSON.parse(stdout);
15135
- if (isRecord3(parsed)) {
15336
+ if (isRecord4(parsed)) {
15136
15337
  text = JSON.stringify(sanitize(parsed)) ?? stdout;
15137
15338
  }
15138
15339
  } catch {
15139
15340
  }
15140
15341
  return text.slice(0, 4096);
15141
15342
  }
15343
+ function buildResidentPayload(args, timeoutMs) {
15344
+ return {
15345
+ handler: args.handler,
15346
+ input: args.input,
15347
+ ...args.connection === void 0 ? {} : { connection: args.connection },
15348
+ ...args.connections === void 0 ? {} : { connections: args.connections },
15349
+ context: args.context,
15350
+ timeoutMs
15351
+ };
15352
+ }
15142
15353
  var SandboxPluginExecutor = class {
15143
15354
  /**
15144
- * @param deps - Sandbox resolver and optional logger
15355
+ * @param deps - Sandbox resolver, optional logger, and optional resident routing
15145
15356
  */
15146
15357
  constructor(deps) {
15147
15358
  this.deps = deps;
15148
15359
  }
15149
15360
  /**
15150
- * Invoke a plugin handler inside the tenant sandbox shell.
15361
+ * Invoke a plugin handler.
15362
+ *
15363
+ * Routes to the published plugin's resident service when {@link
15364
+ * SandboxPluginExecutorDeps.resident} is configured and a deployment is
15365
+ * routable (`running` with an endpoint, or a resumed `onDemand` deployment).
15366
+ * Otherwise, or when the resident call fails and degradation is enabled, runs
15367
+ * the handler as a short `node <handler>` process inside the tenant sandbox.
15368
+ *
15369
+ * @param args - Invocation arguments including manifest, handler, and input
15370
+ * @returns The normalized handler result, exit code, timing, and raw stdout
15371
+ * @throws When `args.handler` is not a safe relative path
15372
+ */
15373
+ async invoke(args) {
15374
+ const started = Date.now();
15375
+ validateRelativePath(args.handler, "handler");
15376
+ if (this.deps.resident) {
15377
+ const resident = await this.tryResident(args, this.deps.resident, started);
15378
+ if (resident) return resident;
15379
+ }
15380
+ return this.invokeShortProcess(args, started);
15381
+ }
15382
+ /**
15383
+ * Attempt to route an invocation to a published plugin's resident service.
15384
+ *
15385
+ * @param args - Invocation arguments
15386
+ * @param resident - Resident routing dependencies
15387
+ * @param started - Invocation start timestamp used for `durationMs`
15388
+ * @returns A terminal result (success, or failure with degradation disabled),
15389
+ * or `undefined` when the caller should fall back to the short-process path
15390
+ */
15391
+ async tryResident(args, resident, started) {
15392
+ const tenantId2 = args.runConfig?.tenantId ?? args.context.tenantId;
15393
+ const pluginType = args.manifest.type;
15394
+ if (!tenantId2) return void 0;
15395
+ const deployment = await this.lookupDeployment(resident, tenantId2, pluginType);
15396
+ if (!deployment) return void 0;
15397
+ const timeoutMs = clamp(args.timeoutMs ?? DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS);
15398
+ const payload = buildResidentPayload(args, timeoutMs);
15399
+ if (deployment.status === "running" && deployment.endpoint) {
15400
+ let outcome = await this.callResident(resident, deployment.endpoint, payload, timeoutMs);
15401
+ if (!outcome.ok && deployment.mode === "onDemand" && resident.ensureRunning) {
15402
+ outcome = await this.ensureRunningAndRetry(
15403
+ resident,
15404
+ tenantId2,
15405
+ pluginType,
15406
+ deployment.endpoint,
15407
+ payload,
15408
+ timeoutMs,
15409
+ outcome
15410
+ );
15411
+ }
15412
+ return this.finalizeResident(outcome, args, resident, pluginType, tenantId2, started);
15413
+ }
15414
+ if (deployment.mode === "onDemand" && resident.ensureRunning) {
15415
+ const outcome = await this.wakeOnDemand(resident, tenantId2, pluginType, payload, timeoutMs);
15416
+ if (outcome) {
15417
+ return this.finalizeResident(outcome, args, resident, pluginType, tenantId2, started);
15418
+ }
15419
+ }
15420
+ return void 0;
15421
+ }
15422
+ /**
15423
+ * Normalize a resident invocation outcome into the caller-facing result.
15424
+ *
15425
+ * @param outcome - Normalized resident transport result
15426
+ * @param args - Invocation arguments (for the result-size cap)
15427
+ * @param resident - Resident routing dependencies (degradation policy)
15428
+ * @param pluginType - Plugin type, for diagnostics
15429
+ * @param tenantId - Tenant owning the plugin, for diagnostics
15430
+ * @param started - Invocation start timestamp used for `durationMs`
15431
+ * @returns The normalized result, or `undefined` when the caller should fall
15432
+ * back to the short-process path
15433
+ */
15434
+ finalizeResident(outcome, args, resident, pluginType, tenantId2, started) {
15435
+ if (outcome.ok) {
15436
+ const cap = clamp(
15437
+ args.maxResultBytes ?? DEFAULT_MAX_RESULT_BYTES,
15438
+ MIN_MAX_RESULT_BYTES,
15439
+ MAX_MAX_RESULT_BYTES
15440
+ );
15441
+ return {
15442
+ ok: true,
15443
+ result: capResult(outcome.result, cap),
15444
+ exitCode: 0,
15445
+ httpStatus: outcome.status,
15446
+ durationMs: Date.now() - started,
15447
+ stdout: "",
15448
+ stderr: ""
15449
+ };
15450
+ }
15451
+ const error = outcome.error ?? "resident plugin invocation failed";
15452
+ if (resident.degradeToShortProcess ?? true) {
15453
+ this.deps.logger?.info(
15454
+ { event: "sandbox-plugin.resident.degrade", pluginType, tenantId: tenantId2, error },
15455
+ "resident plugin invocation failed; falling back to short process"
15456
+ );
15457
+ return void 0;
15458
+ }
15459
+ return {
15460
+ ok: false,
15461
+ error,
15462
+ exitCode: 1,
15463
+ httpStatus: outcome.status,
15464
+ durationMs: Date.now() - started,
15465
+ stdout: "",
15466
+ stderr: ""
15467
+ };
15468
+ }
15469
+ /**
15470
+ * Look up a deployment, degrading to `undefined` when the lookup throws.
15471
+ *
15472
+ * @param resident - Resident routing dependencies
15473
+ * @param tenantId - Tenant owning the plugin
15474
+ * @param pluginType - Plugin type addressed by the invocation
15475
+ * @returns The deployment, or `undefined` when absent or lookup failed
15476
+ */
15477
+ async lookupDeployment(resident, tenantId2, pluginType) {
15478
+ try {
15479
+ return await resident.getDeployment(tenantId2, pluginType);
15480
+ } catch (error) {
15481
+ this.deps.logger?.info(
15482
+ {
15483
+ event: "sandbox-plugin.resident.lookup.failed",
15484
+ pluginType,
15485
+ tenantId: tenantId2,
15486
+ error: error instanceof Error ? error.message : String(error)
15487
+ },
15488
+ "resident deployment lookup failed; falling back to short process"
15489
+ );
15490
+ return void 0;
15491
+ }
15492
+ }
15493
+ /**
15494
+ * Resume a non-running `onDemand` deployment once, then invoke it.
15495
+ *
15496
+ * Re-reads the deployment after the resume so a cold-started endpoint is
15497
+ * picked up. Never throws: a failed resume or a deployment that is still not
15498
+ * `running` with an endpoint resolves to `undefined`, letting the caller use
15499
+ * the short-process path.
15500
+ *
15501
+ * @param resident - Resident routing dependencies
15502
+ * @param tenantId - Tenant owning the plugin
15503
+ * @param pluginType - Plugin type addressed by the invocation
15504
+ * @param payload - Invocation payload
15505
+ * @param timeoutMs - Clamped per-call timeout
15506
+ * @returns The resident outcome when routed, or `undefined` to short-process
15507
+ */
15508
+ async wakeOnDemand(resident, tenantId2, pluginType, payload, timeoutMs) {
15509
+ try {
15510
+ await resident.ensureRunning?.(tenantId2, pluginType);
15511
+ } catch (error) {
15512
+ this.deps.logger?.info(
15513
+ {
15514
+ event: "sandbox-plugin.resident.ensure.failed",
15515
+ pluginType,
15516
+ tenantId: tenantId2,
15517
+ error: error instanceof Error ? error.message : String(error)
15518
+ },
15519
+ "resident service resume failed; falling back to short process"
15520
+ );
15521
+ return void 0;
15522
+ }
15523
+ const refreshed = await this.lookupDeployment(resident, tenantId2, pluginType);
15524
+ if (!refreshed || refreshed.status !== "running" || !refreshed.endpoint) {
15525
+ return void 0;
15526
+ }
15527
+ return this.callResident(resident, refreshed.endpoint, payload, timeoutMs);
15528
+ }
15529
+ /**
15530
+ * Resume an `onDemand` deployment once and retry the resident call once.
15531
+ *
15532
+ * @param resident - Resident routing dependencies
15533
+ * @param tenantId - Tenant owning the plugin
15534
+ * @param pluginType - Plugin type addressed by the invocation
15535
+ * @param endpoint - Endpoint used by the first attempt
15536
+ * @param payload - Invocation payload
15537
+ * @param timeoutMs - Clamped per-call timeout
15538
+ * @param firstOutcome - Failed first attempt, returned when resume fails
15539
+ * @returns The retry outcome, or `firstOutcome` when resume itself failed
15540
+ */
15541
+ async ensureRunningAndRetry(resident, tenantId2, pluginType, endpoint, payload, timeoutMs, firstOutcome) {
15542
+ try {
15543
+ await resident.ensureRunning?.(tenantId2, pluginType);
15544
+ } catch (error) {
15545
+ this.deps.logger?.info(
15546
+ {
15547
+ event: "sandbox-plugin.resident.ensure.failed",
15548
+ pluginType,
15549
+ tenantId: tenantId2,
15550
+ error: error instanceof Error ? error.message : String(error)
15551
+ },
15552
+ "resident service resume failed"
15553
+ );
15554
+ return firstOutcome;
15555
+ }
15556
+ const refreshed = await this.lookupDeployment(resident, tenantId2, pluginType);
15557
+ return this.callResident(resident, refreshed?.endpoint ?? endpoint, payload, timeoutMs);
15558
+ }
15559
+ /**
15560
+ * Invoke the resident host over the sandbox-service proxy path.
15561
+ *
15562
+ * @param resident - Resident routing dependencies
15563
+ * @param endpointPath - Relative proxy path
15564
+ * @param payload - Invocation payload matching the host contract
15565
+ * @param timeoutMs - Clamped per-call timeout
15566
+ * @returns The normalized resident invocation result
15567
+ */
15568
+ callResident(resident, endpointPath, payload, timeoutMs) {
15569
+ return invokeResident({
15570
+ baseUrl: resident.baseUrl,
15571
+ endpointPath,
15572
+ serviceKey: resident.serviceKey,
15573
+ payload,
15574
+ timeoutMs,
15575
+ fetchImpl: resident.fetchImpl
15576
+ });
15577
+ }
15578
+ /**
15579
+ * Invoke a plugin handler as a short `node <handler>` process.
15151
15580
  *
15152
15581
  * Resolves the sandbox, injects small input as a base64 env var (or writes a
15153
15582
  * temp `input.json` for large input), writes connection credentials to a temp
@@ -15157,12 +15586,10 @@ var SandboxPluginExecutor = class {
15157
15586
  * in all cases, including setup failures.
15158
15587
  *
15159
15588
  * @param args - Invocation arguments including manifest, handler, and input
15589
+ * @param started - Invocation start timestamp used for `durationMs`
15160
15590
  * @returns The normalized handler result, exit code, timing, and raw stdout
15161
- * @throws When `args.handler` is not a safe relative path
15162
15591
  */
15163
- async invoke(args) {
15164
- const started = Date.now();
15165
- validateRelativePath(args.handler, "handler");
15592
+ async invokeShortProcess(args, started) {
15166
15593
  const pluginDir = `${PLUGINS_ROOT}/${args.manifest.type}`;
15167
15594
  const handlerAbs = `${pluginDir}/${args.handler}`;
15168
15595
  const tenantId2 = args.runConfig?.tenantId ?? args.context.tenantId;
@@ -15250,11 +15677,9 @@ var SandboxPluginExecutor = class {
15250
15677
  }
15251
15678
  const parsed = parseEnvelope(stdout);
15252
15679
  if (parsed) {
15253
- const result = parsed.ok ? parsed.result : void 0;
15254
- const serialized = typeof result === "string" ? result : JSON.stringify(result ?? null);
15255
15680
  return {
15256
15681
  ok: parsed.ok,
15257
- result: Buffer.byteLength(serialized, "utf8") <= cap ? result : truncate(serialized, cap),
15682
+ result: capResult(parsed.ok ? parsed.result : void 0, cap),
15258
15683
  error: typeof parsed.error === "string" ? parsed.error : void 0,
15259
15684
  exitCode,
15260
15685
  durationMs,
@@ -15265,7 +15690,7 @@ var SandboxPluginExecutor = class {
15265
15690
  const text = stdout.trim();
15266
15691
  return {
15267
15692
  ok: true,
15268
- result: truncate(text, cap),
15693
+ result: capResult(text, cap),
15269
15694
  exitCode,
15270
15695
  durationMs,
15271
15696
  stdout,
@@ -15282,24 +15707,33 @@ var SandboxPluginExecutor = class {
15282
15707
  }
15283
15708
  };
15284
15709
 
15710
+ // src/sandbox_plugin/pluginDeploymentServiceRuntime.ts
15711
+ var service2 = null;
15712
+ function setPluginDeploymentService(instance) {
15713
+ service2 = instance ?? null;
15714
+ }
15715
+ function getPluginDeploymentService() {
15716
+ return service2;
15717
+ }
15718
+
15285
15719
  // src/sandbox_plugin/TenantPluginCatalog.ts
15286
15720
  var import_node_crypto3 = require("crypto");
15287
15721
 
15288
15722
  // src/sandbox_plugin/jsonSchemaToZod.ts
15289
15723
  var import_zod45 = require("zod");
15290
- function isRecord4(value) {
15724
+ function isRecord5(value) {
15291
15725
  return typeof value === "object" && value !== null && !Array.isArray(value);
15292
15726
  }
15293
15727
  function withDescription(schema6, raw) {
15294
- const description = isRecord4(raw) ? raw.description : void 0;
15728
+ const description = isRecord5(raw) ? raw.description : void 0;
15295
15729
  return typeof description === "string" ? schema6.describe(description) : schema6;
15296
15730
  }
15297
15731
  function jsonSchemaToZod(schema6) {
15298
- if (!isRecord4(schema6)) throw new Error("schema must be an object");
15732
+ if (!isRecord5(schema6)) throw new Error("schema must be an object");
15299
15733
  const type = schema6.type;
15300
15734
  switch (type) {
15301
15735
  case "object": {
15302
- const props = isRecord4(schema6.properties) ? schema6.properties : {};
15736
+ const props = isRecord5(schema6.properties) ? schema6.properties : {};
15303
15737
  const required = Array.isArray(schema6.required) ? schema6.required.filter((x) => typeof x === "string") : [];
15304
15738
  const shape = {};
15305
15739
  for (const [key5, sub] of Object.entries(props)) {
@@ -15339,7 +15773,7 @@ var DISPLAY_MODES = ["inline", "fullscreen", "pip"];
15339
15773
  var MAX_MANIFEST_BYTES = 262144;
15340
15774
  var MAX_TOOLS = 32;
15341
15775
  var MAX_PLUGINS_PER_TENANT = 50;
15342
- function isRecord5(value) {
15776
+ function isRecord6(value) {
15343
15777
  return typeof value === "object" && value !== null && !Array.isArray(value);
15344
15778
  }
15345
15779
  function isSafeHandler(path8) {
@@ -15364,7 +15798,7 @@ function validateSandboxPluginManifest(args) {
15364
15798
  const diagnostics = [];
15365
15799
  const error = (code, message, pluginType, tool68) => diagnostics.push({ level: "error", code, message, pluginType, tool: tool68 });
15366
15800
  const raw = args.raw;
15367
- if (!isRecord5(raw)) {
15801
+ if (!isRecord6(raw)) {
15368
15802
  error("MANIFEST_NOT_OBJECT", "manifest must be a JSON object", args.dirName);
15369
15803
  return { tools: [], diagnostics };
15370
15804
  }
@@ -15417,7 +15851,7 @@ function validateSandboxPluginManifest(args) {
15417
15851
  const tools = [];
15418
15852
  const seenToolNames = /* @__PURE__ */ new Set();
15419
15853
  for (const candidate of toolCandidates.slice(0, MAX_TOOLS)) {
15420
- if (!isRecord5(candidate)) {
15854
+ if (!isRecord6(candidate)) {
15421
15855
  error("TOOL_INVALID", "tool must be an object", type);
15422
15856
  continue;
15423
15857
  }
@@ -15434,7 +15868,7 @@ function validateSandboxPluginManifest(args) {
15434
15868
  error("HANDLER_MISSING", `handler "${String(candidate.handler)}" not found`, type, name);
15435
15869
  continue;
15436
15870
  }
15437
- if (!isRecord5(candidate.schema)) {
15871
+ if (!isRecord6(candidate.schema)) {
15438
15872
  error("TOOL_SCHEMA", `tool "${name}" schema must be an object`, type, name);
15439
15873
  continue;
15440
15874
  }
@@ -15448,17 +15882,17 @@ function validateSandboxPluginManifest(args) {
15448
15882
  let ui;
15449
15883
  if (candidate.ui !== void 0) {
15450
15884
  const rawUi = candidate.ui;
15451
- if (!isRecord5(rawUi) || !isSafeUiPath(rawUi.resource) || !args.handlerExists(rawUi.resource)) {
15885
+ if (!isRecord6(rawUi) || !isSafeUiPath(rawUi.resource) || !args.handlerExists(rawUi.resource)) {
15452
15886
  error("TOOL_UI", `tool "${name}" ui.resource must be an existing .html file within the plugin dir`, type, name);
15453
15887
  } else {
15454
- const csp = isRecord5(rawUi.csp) ? {
15888
+ const csp = isRecord6(rawUi.csp) ? {
15455
15889
  connectDomains: Array.isArray(rawUi.csp.connectDomains) ? rawUi.csp.connectDomains.filter((d) => typeof d === "string") : void 0,
15456
15890
  resourceDomains: Array.isArray(rawUi.csp.resourceDomains) ? rawUi.csp.resourceDomains.filter((d) => typeof d === "string") : void 0,
15457
15891
  frameDomains: Array.isArray(rawUi.csp.frameDomains) ? rawUi.csp.frameDomains.filter((d) => typeof d === "string") : void 0,
15458
15892
  baseUriDomains: Array.isArray(rawUi.csp.baseUriDomains) ? rawUi.csp.baseUriDomains.filter((d) => typeof d === "string") : void 0
15459
15893
  } : void 0;
15460
15894
  const displayMode = DISPLAY_MODES.includes(rawUi.displayMode) ? rawUi.displayMode : void 0;
15461
- const permissions = isRecord5(rawUi.permissions) ? {
15895
+ const permissions = isRecord6(rawUi.permissions) ? {
15462
15896
  camera: rawUi.permissions.camera === true ? true : void 0,
15463
15897
  microphone: rawUi.permissions.microphone === true ? true : void 0,
15464
15898
  geolocation: rawUi.permissions.geolocation === true ? true : void 0,
@@ -15488,7 +15922,7 @@ function validateSandboxPluginManifest(args) {
15488
15922
  }
15489
15923
  const skills = {};
15490
15924
  if (raw.skills !== void 0) {
15491
- if (!isRecord5(raw.skills)) {
15925
+ if (!isRecord6(raw.skills)) {
15492
15926
  error("SKILLS_INVALID", "skills must be an object", type);
15493
15927
  } else {
15494
15928
  const prefix = `${type}-`;
@@ -15503,12 +15937,12 @@ function validateSandboxPluginManifest(args) {
15503
15937
  error("SKILL_NAME", `skill "${name}" must be a safe relative path prefixed with "${prefix}"`, type);
15504
15938
  continue;
15505
15939
  }
15506
- if (!isRecord5(definition) || typeof definition.version !== "string" || typeof definition.content !== "string") {
15940
+ if (!isRecord6(definition) || typeof definition.version !== "string" || typeof definition.content !== "string") {
15507
15941
  error("SKILL_INVALID", `skill "${name}" requires string version and content`, type);
15508
15942
  continue;
15509
15943
  }
15510
15944
  if (definition.resources !== void 0) {
15511
- if (!isRecord5(definition.resources)) {
15945
+ if (!isRecord6(definition.resources)) {
15512
15946
  error("SKILL_RESOURCES", `skill "${name}" resources must be an object`, type);
15513
15947
  continue;
15514
15948
  }
@@ -15520,7 +15954,7 @@ function validateSandboxPluginManifest(args) {
15520
15954
  resourcesOk = false;
15521
15955
  break;
15522
15956
  }
15523
- if (!isRecord5(resource) || typeof resource.content !== "string") {
15957
+ if (!isRecord6(resource) || typeof resource.content !== "string") {
15524
15958
  resourcesOk = false;
15525
15959
  break;
15526
15960
  }
@@ -15536,11 +15970,11 @@ function validateSandboxPluginManifest(args) {
15536
15970
  }
15537
15971
  const agents = {};
15538
15972
  if (raw.agents !== void 0) {
15539
- if (!isRecord5(raw.agents)) {
15973
+ if (!isRecord6(raw.agents)) {
15540
15974
  error("AGENTS_INVALID", "agents must be an object", type);
15541
15975
  } else {
15542
15976
  for (const [key5, config] of Object.entries(raw.agents)) {
15543
- if (!isRecord5(config)) {
15977
+ if (!isRecord6(config)) {
15544
15978
  error("AGENT_INVALID", `agent "${key5}" must be an object`, type);
15545
15979
  continue;
15546
15980
  }
@@ -15561,7 +15995,7 @@ function validateSandboxPluginManifest(args) {
15561
15995
  }
15562
15996
  }
15563
15997
  let connection;
15564
- if (isRecord5(raw.connection)) {
15998
+ if (isRecord6(raw.connection)) {
15565
15999
  const fields = raw.connection.fields;
15566
16000
  if (!Array.isArray(fields)) {
15567
16001
  error("CONNECTION_INVALID", "connection.fields must be an array", type);
@@ -15575,7 +16009,7 @@ function validateSandboxPluginManifest(args) {
15575
16009
  for (const key5 of ["test", "discover"]) {
15576
16010
  const entry = raw.connection[key5];
15577
16011
  if (entry === void 0) continue;
15578
- const handler = isRecord5(entry) ? entry.handler : void 0;
16012
+ const handler = isRecord6(entry) ? entry.handler : void 0;
15579
16013
  if (!isSafeHandler(handler) || !args.handlerExists(handler)) {
15580
16014
  error("CONNECTION_HANDLER", `connection.${key5} handler "${String(handler)}" not found`, type);
15581
16015
  continue;
@@ -15593,8 +16027,8 @@ function validateSandboxPluginManifest(args) {
15593
16027
  version: raw.version,
15594
16028
  icon: typeof raw.icon === "string" ? raw.icon : void 0,
15595
16029
  category: typeof raw.category === "string" ? raw.category : void 0,
15596
- configSchema: isRecord5(raw.configSchema) ? raw.configSchema : void 0,
15597
- defaultConfig: isRecord5(raw.defaultConfig) ? raw.defaultConfig : void 0,
16030
+ configSchema: isRecord6(raw.configSchema) ? raw.configSchema : void 0,
16031
+ defaultConfig: isRecord6(raw.defaultConfig) ? raw.defaultConfig : void 0,
15598
16032
  connection,
15599
16033
  tools,
15600
16034
  skills: Object.keys(skills).length > 0 ? skills : void 0,
@@ -15774,7 +16208,7 @@ var STANDARD_CONNECTION_DEFAULT_CONFIG = Object.freeze({
15774
16208
  connectAll: false
15775
16209
  });
15776
16210
  var EMPTY_DEFAULT_CONFIG = Object.freeze({});
15777
- function isRecord6(value) {
16211
+ function isRecord7(value) {
15778
16212
  return typeof value === "object" && value !== null && !Array.isArray(value);
15779
16213
  }
15780
16214
  function runConfigFrom(exeConfig) {
@@ -15826,7 +16260,7 @@ function compileSandboxPlugin(args) {
15826
16260
  maxResultBytes: t.maxResultBytes
15827
16261
  });
15828
16262
  if (!result.ok) return result.error ?? "sandbox plugin execution failed";
15829
- const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result ?? null);
16263
+ const text = handlerResultText(result);
15830
16264
  const ref = t.ui ? { kind: "plugin", pluginType: manifest.type, resource: t.ui.resource, displayMode: t.ui.displayMode ?? "inline" } : null;
15831
16265
  return ref ? appendUiFence(text, ref) : text;
15832
16266
  },
@@ -15864,11 +16298,11 @@ async function runConnectionHandler(executor, manifest, handler, config, tenantI
15864
16298
  return { ok: false, message: result.error ?? "connection test failed" };
15865
16299
  }
15866
16300
  const value = result.result;
15867
- if (isRecord6(value) && typeof value.ok === "boolean" && typeof value.message === "string") {
16301
+ if (isRecord7(value) && typeof value.ok === "boolean" && typeof value.message === "string") {
15868
16302
  return {
15869
16303
  ok: value.ok,
15870
16304
  message: value.message,
15871
- details: isRecord6(value.details) ? value.details : void 0
16305
+ details: isRecord7(value.details) ? value.details : void 0
15872
16306
  };
15873
16307
  }
15874
16308
  return { ok: false, message: result.error ?? "connection test returned an invalid result" };
@@ -16221,13 +16655,29 @@ var TenantPluginCatalog = class {
16221
16655
 
16222
16656
  // src/sandbox_plugin/sandboxPluginRuntime.ts
16223
16657
  var catalog = null;
16658
+ function buildResidentRouting() {
16659
+ const baseUrl = process.env.MICROSANDBOX_SERVICE_BASE_URL;
16660
+ if (!baseUrl) return void 0;
16661
+ return {
16662
+ getDeployment: async (tenantId2, pluginType) => await getPluginDeploymentService()?.getDeployment(tenantId2, pluginType) ?? void 0,
16663
+ ensureRunning: async (tenantId2, pluginType) => {
16664
+ await getPluginDeploymentService()?.ensureRunning(tenantId2, pluginType);
16665
+ },
16666
+ baseUrl,
16667
+ serviceKey: process.env.MICROSANDBOX_API_KEY
16668
+ };
16669
+ }
16670
+ function buildExecutor(resolveSandbox) {
16671
+ const resident = buildResidentRouting();
16672
+ return new SandboxPluginExecutor({ resolveSandbox, ...resident ? { resident } : {} });
16673
+ }
16224
16674
  function emptyCatalog() {
16225
16675
  const resolveSandbox = async () => {
16226
16676
  throw new Error("sandbox manager not configured");
16227
16677
  };
16228
16678
  return new TenantPluginCatalog({
16229
16679
  resolveSandbox,
16230
- executor: new SandboxPluginExecutor({ resolveSandbox }),
16680
+ executor: buildExecutor(resolveSandbox),
16231
16681
  reservedTypes: () => new Set(PluginRegistry.list())
16232
16682
  });
16233
16683
  }
@@ -16244,7 +16694,7 @@ function getSandboxPluginCatalog() {
16244
16694
  const resolveSandbox = createSandboxResolver((config) => manager.getSandboxFromConfig(config));
16245
16695
  catalog = new TenantPluginCatalog({
16246
16696
  resolveSandbox,
16247
- executor: new SandboxPluginExecutor({ resolveSandbox }),
16697
+ executor: buildExecutor(resolveSandbox),
16248
16698
  reservedTypes: () => new Set(PluginRegistry.list())
16249
16699
  });
16250
16700
  return catalog;
@@ -16391,23 +16841,23 @@ function toolName(tool68) {
16391
16841
  return typeof tool68.name === "string" ? tool68.name : void 0;
16392
16842
  }
16393
16843
  function schemaHasConnectionKey(schema6) {
16394
- const definition = isRecord7(schema6._def) ? schema6._def : void 0;
16844
+ const definition = isRecord8(schema6._def) ? schema6._def : void 0;
16395
16845
  const shape = definition?.shape;
16396
16846
  const fields = typeof shape === "function" ? shape() : shape;
16397
- return isRecord7(fields) && Object.prototype.hasOwnProperty.call(fields, "connectionKey");
16847
+ return isRecord8(fields) && Object.prototype.hasOwnProperty.call(fields, "connectionKey");
16398
16848
  }
16399
16849
  function hasConnectionKeySchema(tool68) {
16400
- if (!isRecord7(tool68) || !isRecord7(tool68.schema)) return false;
16850
+ if (!isRecord8(tool68) || !isRecord8(tool68.schema)) return false;
16401
16851
  if (schemaHasConnectionKey(tool68.schema)) return true;
16402
- const definition = isRecord7(tool68.schema._def) ? tool68.schema._def : void 0;
16852
+ const definition = isRecord8(tool68.schema._def) ? tool68.schema._def : void 0;
16403
16853
  if (Array.isArray(definition?.options) && definition.options.length > 0) {
16404
16854
  return definition.options.every(
16405
- (option) => isRecord7(option) && schemaHasConnectionKey(option)
16855
+ (option) => isRecord8(option) && schemaHasConnectionKey(option)
16406
16856
  );
16407
16857
  }
16408
16858
  return false;
16409
16859
  }
16410
- function isRecord7(value) {
16860
+ function isRecord8(value) {
16411
16861
  return typeof value === "object" && value !== null && !Array.isArray(value);
16412
16862
  }
16413
16863
  var CapabilityToolRegistry = class {
@@ -16499,17 +16949,17 @@ var CapabilityToolRegistry = class {
16499
16949
  // src/middlewares/capabilityGuardMiddleware.ts
16500
16950
  var import_messages4 = require("@langchain/core/messages");
16501
16951
  var import_langchain49 = require("langchain");
16502
- function isRecord8(value) {
16952
+ function isRecord9(value) {
16503
16953
  return typeof value === "object" && value !== null && !Array.isArray(value);
16504
16954
  }
16505
16955
  function capabilityRuntimeFromRequest(request) {
16506
- if (!isRecord8(request) || !isRecord8(request.runtime)) return void 0;
16956
+ if (!isRecord9(request) || !isRecord9(request.runtime)) return void 0;
16507
16957
  const configurable = request.runtime.configurable;
16508
16958
  const context = request.runtime.context;
16509
- const runConfig = isRecord8(configurable) && isRecord8(configurable.runConfig) ? configurable.runConfig : isRecord8(context) && isRecord8(context.runConfig) ? context.runConfig : void 0;
16959
+ const runConfig = isRecord9(configurable) && isRecord9(configurable.runConfig) ? configurable.runConfig : isRecord9(context) && isRecord9(context.runConfig) ? context.runConfig : void 0;
16510
16960
  if (!runConfig) return void 0;
16511
16961
  const value = runConfig.capabilityRuntime;
16512
- if (!isRecord8(value) || typeof value.revision !== "string" || !isRecord8(value.middleware)) {
16962
+ if (!isRecord9(value) || typeof value.revision !== "string" || !isRecord9(value.middleware)) {
16513
16963
  return void 0;
16514
16964
  }
16515
16965
  return value;
@@ -16521,10 +16971,10 @@ function hasActiveRuntimePolicy(runtime) {
16521
16971
  return Object.keys(runtime.middleware).length > 0;
16522
16972
  }
16523
16973
  function tenantIdFromRequest(request) {
16524
- if (!isRecord8(request) || !isRecord8(request.runtime)) return void 0;
16974
+ if (!isRecord9(request) || !isRecord9(request.runtime)) return void 0;
16525
16975
  const configurable = request.runtime.configurable;
16526
16976
  const context = request.runtime.context;
16527
- const runConfig = isRecord8(configurable) && isRecord8(configurable.runConfig) ? configurable.runConfig : isRecord8(context) && isRecord8(context.runConfig) ? context.runConfig : void 0;
16977
+ const runConfig = isRecord9(configurable) && isRecord9(configurable.runConfig) ? configurable.runConfig : isRecord9(context) && isRecord9(context.runConfig) ? context.runConfig : void 0;
16528
16978
  return runConfig && typeof runConfig.tenantId === "string" ? runConfig.tenantId : void 0;
16529
16979
  }
16530
16980
  async function standardConnectionAllowed(request, capabilityType, config) {
@@ -16532,8 +16982,8 @@ async function standardConnectionAllowed(request, capabilityType, config) {
16532
16982
  const hasConnectAll = Object.prototype.hasOwnProperty.call(config, "connectAll");
16533
16983
  if (!hasConnections && (!hasConnectAll || config.connectAll !== true)) return false;
16534
16984
  const tenantId2 = tenantIdFromRequest(request);
16535
- const resource = isRecord8(request.toolCall) && isRecord8(request.toolCall.args) ? request.toolCall.args.connectionKey : void 0;
16536
- const hasConnectionKey = isRecord8(request.toolCall) && isRecord8(request.toolCall.args) && Object.prototype.hasOwnProperty.call(
16985
+ const resource = isRecord9(request.toolCall) && isRecord9(request.toolCall.args) ? request.toolCall.args.connectionKey : void 0;
16986
+ const hasConnectionKey = isRecord9(request.toolCall) && isRecord9(request.toolCall.args) && Object.prototype.hasOwnProperty.call(
16537
16987
  request.toolCall.args,
16538
16988
  "connectionKey"
16539
16989
  );
@@ -16599,10 +17049,10 @@ function createCapabilityGuardMiddleware(registry4) {
16599
17049
  toolName: toolName2
16600
17050
  });
16601
17051
  }
16602
- const standardConfig = isRecord8(policy.config) ? policy.config : {};
17052
+ const standardConfig = isRecord9(policy.config) ? policy.config : {};
16603
17053
  if (registry4.isStandardConnectionTool(toolName2) && !await standardConnectionAllowed(request, metadata.capabilityType, standardConfig)) {
16604
17054
  const args2 = request.toolCall.args;
16605
- const resource = isRecord8(args2) && typeof args2.connectionKey === "string" ? args2.connectionKey : void 0;
17055
+ const resource = isRecord9(args2) && typeof args2.connectionKey === "string" ? args2.connectionKey : void 0;
16606
17056
  return denialMessage(request.toolCall, {
16607
17057
  code: "RESOURCE_NOT_ALLOWED",
16608
17058
  message: typeof resource === "string" ? `Resource '${resource}' is not allowed by the capability policy` : "Resource selector 'connectionKey' must be a permitted string",
@@ -16615,8 +17065,8 @@ function createCapabilityGuardMiddleware(registry4) {
16615
17065
  const selector = metadata.resourceSelector;
16616
17066
  const args = request.toolCall.args;
16617
17067
  if (selector) {
16618
- const resource = isRecord8(args) ? args[selector.argumentField] : void 0;
16619
- const config = isRecord8(policy.config) ? policy.config : {};
17068
+ const resource = isRecord9(args) ? args[selector.argumentField] : void 0;
17069
+ const config = isRecord9(policy.config) ? policy.config : {};
16620
17070
  const hasExplicitResources = Object.prototype.hasOwnProperty.call(config, selector.configField);
16621
17071
  const allowsAll = !hasExplicitResources && selector.allowAllField !== void 0 && config[selector.allowAllField] === true;
16622
17072
  const allowedResources = config[selector.configField];
@@ -32391,6 +32841,7 @@ function assistantToConfig(assistant) {
32391
32841
  prompt: typeof assistant.graphDefinition === "string" ? assistant.graphDefinition : JSON.stringify(assistant.graphDefinition)
32392
32842
  };
32393
32843
  Object.assign(config, graphDef);
32844
+ if (assistant.ownerUserId !== void 0) config.ownerUserId = assistant.ownerUserId;
32394
32845
  return config;
32395
32846
  }
32396
32847
  var _AgentLatticeManager = class _AgentLatticeManager extends BaseLatticeManager {
@@ -34788,6 +35239,9 @@ var MicrosandboxRemoteInstance = class {
34788
35239
  }
34789
35240
  return "unknown";
34790
35241
  }
35242
+ async getEndpoint(port) {
35243
+ return this.client.getEndpoint(this.name, port);
35244
+ }
34791
35245
  /**
34792
35246
  * Pack a sandbox folder into a sibling ZIP through the microsandbox shell.
34793
35247
  *
@@ -34854,6 +35308,19 @@ var MicrosandboxServiceClient = class {
34854
35308
  method: "GET"
34855
35309
  });
34856
35310
  }
35311
+ /**
35312
+ * Resolve the endpoint for a published guest port of a sandbox.
35313
+ *
35314
+ * @param name - Sandbox name
35315
+ * @param port - Guest port published via `ensureSandbox({ exposePorts })`
35316
+ * @returns The scheme-less endpoint string and optional headers
35317
+ */
35318
+ async getEndpoint(name, port) {
35319
+ return this.request(
35320
+ `/api/sandboxes/${encodeURIComponent(name)}/endpoints/${port}`,
35321
+ { method: "GET" }
35322
+ );
35323
+ }
34857
35324
  async readFile(sandboxName, path8) {
34858
35325
  return this.request("/api/files/read", {
34859
35326
  method: "POST",
@@ -35009,7 +35476,9 @@ var MicrosandboxServiceClient = class {
35009
35476
  }
35010
35477
  } catch {
35011
35478
  }
35012
- throw new Error(`Microsandbox service request failed: ${detail}`);
35479
+ const error = new Error(`Microsandbox service request failed: ${detail}`);
35480
+ error.statusCode = response.status;
35481
+ throw error;
35013
35482
  }
35014
35483
  const payload = await response.json();
35015
35484
  if (payload?.success === false) {
@@ -35118,9 +35587,9 @@ var MicrosandboxRemoteProvider = class {
35118
35587
  }
35119
35588
  buildEnsureInput(config) {
35120
35589
  const defaultMicrosandboxRemoteConfig = getDefaultMicrosandboxRemoteConfig();
35121
- const image = this.config.image ?? defaultMicrosandboxRemoteConfig.image;
35590
+ const image = config.image ?? this.config.image ?? defaultMicrosandboxRemoteConfig.image;
35122
35591
  console.log(
35123
- `[MicrosandboxRemote] buildEnsureInput image=${image} from=${this.config.image ? "providerConfig" : "defaultConfig"}`
35592
+ `[MicrosandboxRemote] buildEnsureInput image=${image} from=${config.image ? "runConfig" : this.config.image ? "providerConfig" : "defaultConfig"}`
35124
35593
  );
35125
35594
  return {
35126
35595
  image,
@@ -35130,7 +35599,10 @@ var MicrosandboxRemoteProvider = class {
35130
35599
  ...this.config.env,
35131
35600
  ...buildSandboxMetadataEnv(config)
35132
35601
  },
35133
- volumes: config?.volumes ?? this.buildDefaultVolumes(config)
35602
+ volumes: config?.volumes ?? this.buildDefaultVolumes(config),
35603
+ entrypoint: config.entrypoint,
35604
+ timeoutSeconds: config.timeoutSeconds,
35605
+ exposePorts: config.exposePorts
35134
35606
  };
35135
35607
  }
35136
35608
  buildDefaultVolumes(config) {
@@ -36856,6 +37328,157 @@ async function connectAllChannels(getAdapter, options = {}) {
36856
37328
  }
36857
37329
  }
36858
37330
  }
37331
+ async function disconnectAllChannels(getAdapter) {
37332
+ const store = getStoreLattice("default", "channelInstallation").store;
37333
+ const installations = await store.getAllInstallations();
37334
+ console.log(`[disconnectAllChannels] total=${installations.length} enabled=${installations.filter((i) => i.enabled).length}`);
37335
+ for (const inst of installations) {
37336
+ if (!inst.enabled) continue;
37337
+ const adapter = getAdapter(inst.channel);
37338
+ if (!adapter?.disconnect) continue;
37339
+ try {
37340
+ await adapter.disconnect(inst.id);
37341
+ } catch (error) {
37342
+ console.error(`[disconnectAllChannels] disconnect failed: channel=${inst.channel} tenant=${inst.tenantId} id=${inst.id}`, error);
37343
+ }
37344
+ }
37345
+ }
37346
+
37347
+ // src/channel/ChannelConnectionLeader.ts
37348
+ var import_node_crypto8 = require("crypto");
37349
+ var LEASE_KEY = "leader:channel-connections";
37350
+ var ChannelConnectionLeader = class {
37351
+ constructor(deps) {
37352
+ this.holder = null;
37353
+ this.connected = false;
37354
+ this.attempted = false;
37355
+ this.inFlight = null;
37356
+ this.timer = null;
37357
+ this.running = false;
37358
+ this.stopped = false;
37359
+ this.deps = deps;
37360
+ this.ttlMs = deps.ttlMs ?? 6e4;
37361
+ this.retryMs = deps.retryMs ?? Math.max(5e3, Math.floor(this.ttlMs / 3));
37362
+ }
37363
+ /** Starts the ownership loop (immediate tick, then every `retryMs`). */
37364
+ start() {
37365
+ if (this.timer || this.stopped) return;
37366
+ void this.tickOnce();
37367
+ this.timer = setInterval(() => {
37368
+ void this.tickOnce();
37369
+ }, this.retryMs);
37370
+ this.timer.unref?.();
37371
+ }
37372
+ /**
37373
+ * Releases ownership (if held) and disconnects.
37374
+ *
37375
+ * @returns A promise that settles after teardown completes.
37376
+ * @remarks Terminal: a stopped leader does not restart.
37377
+ */
37378
+ async stop() {
37379
+ this.stopped = true;
37380
+ if (this.timer) {
37381
+ clearInterval(this.timer);
37382
+ this.timer = null;
37383
+ }
37384
+ if (this.inFlight) await this.inFlight.catch(() => void 0);
37385
+ if (this.holder) {
37386
+ const holder = this.holder;
37387
+ this.holder = null;
37388
+ await this.deps.leaseStore.release(LEASE_KEY, holder).catch((error) => {
37389
+ this.deps.log?.warn("Channel leader lease release failed", {
37390
+ error: error instanceof Error ? error.message : String(error)
37391
+ });
37392
+ });
37393
+ }
37394
+ await this.disconnectChannels();
37395
+ }
37396
+ /**
37397
+ * Runs one ownership/health step. Public for deterministic tests.
37398
+ *
37399
+ * @returns A promise that settles when this step completes.
37400
+ */
37401
+ async tickOnce() {
37402
+ if (this.running || this.stopped) return;
37403
+ this.running = true;
37404
+ const run = this.runTick();
37405
+ this.inFlight = run;
37406
+ try {
37407
+ await run;
37408
+ } finally {
37409
+ if (this.inFlight === run) this.inFlight = null;
37410
+ this.running = false;
37411
+ }
37412
+ }
37413
+ /**
37414
+ * Performs one acquire/renew/step decision.
37415
+ *
37416
+ * @remarks A transient lease-store error while renewing keeps the current
37417
+ * holder and connections intact (retry next tick); only a confirmed
37418
+ * `renewed === false` triggers teardown.
37419
+ */
37420
+ async runTick() {
37421
+ if (this.holder) {
37422
+ let renewed = false;
37423
+ let renewFailed = false;
37424
+ try {
37425
+ renewed = await this.deps.leaseStore.renew(LEASE_KEY, this.holder, this.ttlMs);
37426
+ } catch (error) {
37427
+ renewFailed = true;
37428
+ this.deps.log?.warn("Channel leader lease renew failed", {
37429
+ error: error instanceof Error ? error.message : String(error)
37430
+ });
37431
+ }
37432
+ if (renewFailed) return;
37433
+ if (renewed) {
37434
+ if (!this.connected) await this.connectChannels();
37435
+ return;
37436
+ }
37437
+ await this.loseLeadership();
37438
+ }
37439
+ await this.tryAcquire();
37440
+ }
37441
+ async tryAcquire() {
37442
+ const holder = `${this.deps.owner}#${(0, import_node_crypto8.randomUUID)()}`;
37443
+ const acquired = await this.deps.leaseStore.tryAcquire(LEASE_KEY, holder, this.ttlMs).catch((error) => {
37444
+ this.deps.log?.warn("Channel leader lease acquire failed", {
37445
+ error: error instanceof Error ? error.message : String(error)
37446
+ });
37447
+ return false;
37448
+ });
37449
+ if (!acquired) return;
37450
+ this.holder = holder;
37451
+ await this.connectChannels();
37452
+ }
37453
+ async loseLeadership() {
37454
+ this.holder = null;
37455
+ await this.disconnectChannels();
37456
+ }
37457
+ async connectChannels() {
37458
+ this.attempted = true;
37459
+ try {
37460
+ await this.deps.connect();
37461
+ this.connected = true;
37462
+ } catch (error) {
37463
+ this.connected = false;
37464
+ this.deps.log?.warn("Channel leader connect failed", {
37465
+ error: error instanceof Error ? error.message : String(error)
37466
+ });
37467
+ }
37468
+ }
37469
+ async disconnectChannels() {
37470
+ if (!this.attempted) return;
37471
+ this.attempted = false;
37472
+ this.connected = false;
37473
+ try {
37474
+ await this.deps.disconnect();
37475
+ } catch (error) {
37476
+ this.deps.log?.warn("Channel leader disconnect failed", {
37477
+ error: error instanceof Error ? error.message : String(error)
37478
+ });
37479
+ }
37480
+ }
37481
+ };
36859
37482
 
36860
37483
  // src/eval_lattice/LatticeEval.ts
36861
37484
  var import_messages7 = require("@langchain/core/messages");
@@ -38304,6 +38927,7 @@ var OutboxPoller = class {
38304
38927
  };
38305
38928
 
38306
38929
  // src/services/ProjectMutationMutex.ts
38930
+ var import_node_async_hooks = require("async_hooks");
38307
38931
  var import_protocols28 = require("@axiom-lattice/protocols");
38308
38932
 
38309
38933
  // src/services/coordinationLease.ts
@@ -38316,7 +38940,7 @@ function getCoordinationLeaseStore() {
38316
38940
  }
38317
38941
 
38318
38942
  // src/services/withLease.ts
38319
- var import_node_crypto8 = require("crypto");
38943
+ var import_node_crypto9 = require("crypto");
38320
38944
  var LeaseAcquisitionTimeoutError = class extends Error {
38321
38945
  /**
38322
38946
  * @param key Lease key that could not be acquired.
@@ -38341,7 +38965,7 @@ async function withLease(store, key5, ownerLabel, fn, options = {}) {
38341
38965
  const acquireTimeoutMs = options.acquireTimeoutMs ?? 1e4;
38342
38966
  const retryDelayMs = options.retryDelayMs ?? 50;
38343
38967
  const heartbeatMs = options.heartbeatMs ?? Math.max(1e3, Math.floor(ttlMs / 3));
38344
- const holder = `${ownerLabel}#${(0, import_node_crypto8.randomUUID)()}`;
38968
+ const holder = `${ownerLabel}#${(0, import_node_crypto9.randomUUID)()}`;
38345
38969
  const deadline = Date.now() + acquireTimeoutMs;
38346
38970
  for (; ; ) {
38347
38971
  if (await store.tryAcquire(key5, holder, ttlMs)) break;
@@ -38373,16 +38997,24 @@ var ProjectMutationMutex = class {
38373
38997
  constructor() {
38374
38998
  this.queues = /* @__PURE__ */ new Map();
38375
38999
  this.active = /* @__PURE__ */ new Set();
39000
+ /** Project keys already held by the current async context (reentrancy). */
39001
+ this.held = new import_node_async_hooks.AsyncLocalStorage();
38376
39002
  }
38377
39003
  /** Runs one mutation after all earlier mutations for the same Tenant and Project. */
38378
39004
  async runExclusive(scope, operation, signal) {
38379
39005
  const key5 = projectKey(scope);
39006
+ const inherited = this.held.getStore();
39007
+ if (inherited?.has(key5)) return operation();
38380
39008
  const leaseKey = `project-mutation:${encodeURIComponent(scope.tenantId)}:${encodeURIComponent(scope.projectId)}`;
38381
39009
  await this.acquire(key5, signal);
39010
+ const held = new Set(inherited);
39011
+ held.add(key5);
38382
39012
  try {
38383
- const store = getCoordinationLeaseStore();
38384
- if (!store) return await operation();
38385
- return await withLease(store, leaseKey, getReplicaId(), operation, MUTATION_LEASE_OPTIONS);
39013
+ return await this.held.run(held, async () => {
39014
+ const store = getCoordinationLeaseStore();
39015
+ if (!store) return await operation();
39016
+ return await withLease(store, leaseKey, getReplicaId(), operation, MUTATION_LEASE_OPTIONS);
39017
+ });
38386
39018
  } finally {
38387
39019
  this.release(key5);
38388
39020
  }
@@ -38456,6 +39088,7 @@ var INPUT_KEYS = [
38456
39088
  ];
38457
39089
  var REPLY_INPUT_KEYS = ["tenantId", "membershipId", "text", "sourceRoomMessageId", "inputMessageId", "sourceId", "idempotencyKey"];
38458
39090
  var INTERRUPT_INPUT_KEYS = ["tenantId", "membershipId", "text", "sourceRoomMessageId", "inputMessageId"];
39091
+ var CORRELATED_INTERRUPT_INPUT_KEYS = ["tenantId", "membershipId", "correlationKey", "text"];
38459
39092
  var RoomAgentMessageService = class {
38460
39093
  /**
38461
39094
  * Creates a room writer over the durable membership and message stores.
@@ -38503,6 +39136,50 @@ var RoomAgentMessageService = class {
38503
39136
  idempotencyKey: `room-interrupt:${snapshot.sourceRoomMessageId}:${snapshot.membershipId}:${sourceId}`
38504
39137
  });
38505
39138
  }
39139
+ /**
39140
+ * Persists a correlated HITL interrupt that has no human source message.
39141
+ *
39142
+ * @param input Explicit tenant, active membership, caller-supplied correlation key, and text.
39143
+ * @returns A deep clone of the canonical idempotent room message.
39144
+ * @remarks Business-agnostic: the caller owns the correlation key. Scope is
39145
+ * validated against the durable membership; no source message is required.
39146
+ */
39147
+ postCorrelatedInterrupt(input) {
39148
+ const snapshot = exactStringRecord(input, CORRELATED_INTERRUPT_INPUT_KEYS, "Invalid project room interrupt input");
39149
+ return this.persistScoped({
39150
+ tenantId: snapshot.tenantId,
39151
+ membershipId: snapshot.membershipId,
39152
+ text: snapshot.text,
39153
+ sourceId: `interrupt:${snapshot.correlationKey}`,
39154
+ idempotencyKey: `room-interrupt:${snapshot.membershipId}:${snapshot.correlationKey}`
39155
+ });
39156
+ }
39157
+ async persistScoped(input) {
39158
+ const text = input.text.trim();
39159
+ if (text.length < 1 || text.length > 2e4) throw new Error("Invalid project room message text");
39160
+ const member = snapshotMembership(await this.deps.memberships.findById(input.tenantId, input.membershipId));
39161
+ if (!member || member.id !== input.membershipId || member.status !== "active" || member.tenantId !== input.tenantId) {
39162
+ throw new Error("Invalid project room membership");
39163
+ }
39164
+ const candidate = {
39165
+ id: (this.deps.idFactory ?? import_crypto8.randomUUID)(),
39166
+ tenantId: member.tenantId,
39167
+ workspaceId: member.workspaceId,
39168
+ projectId: member.projectId,
39169
+ roomId: member.roomId,
39170
+ author: { type: "bot", membershipId: member.id, assistantId: member.assistantId },
39171
+ content: { type: "text", text },
39172
+ mentions: [],
39173
+ source: "agent",
39174
+ sourceId: input.sourceId,
39175
+ idempotencyKey: input.idempotencyKey
39176
+ };
39177
+ const canonical = snapshotCanonicalScoped(await this.deps.messages.createIdempotent(candidate), candidate);
39178
+ if (!canonical) throw new Error("Invalid canonical project room message");
39179
+ const result = structuredClone(canonical);
39180
+ this.notifyCommitted(result);
39181
+ return result;
39182
+ }
38506
39183
  async persistDerived(input) {
38507
39184
  const source = snapshotSource(await this.deps.messages.findById(input.tenantId, input.sourceRoomMessageId));
38508
39185
  if (!source || source.id !== input.sourceRoomMessageId || source.tenantId !== input.tenantId) throw new Error("Invalid project room message");
@@ -38658,6 +39335,27 @@ function snapshotCanonical(value, expected) {
38658
39335
  createdAt
38659
39336
  };
38660
39337
  }
39338
+ function snapshotCanonicalScoped(value, expected) {
39339
+ const values = exactValues(value, ["id", "tenantId", "workspaceId", "projectId", "roomId", "author", "content", "mentions", "source", "sourceId", "idempotencyKey", "createdAt"], ["replyToMessageId"]);
39340
+ const author = exactValues(values?.author, ["type", "membershipId", "assistantId"]);
39341
+ const content = exactValues(values?.content, ["type", "text"]);
39342
+ const createdAt = date(values?.createdAt);
39343
+ if (!values || !createdAt || typeof values.id !== "string" || values.id.length === 0 || (0, import_protocols29.snapshotExactArray)(values.mentions)?.length !== 0 || author?.type !== "bot" || expected.author.type !== "bot" || author.membershipId !== expected.author.membershipId || author.assistantId !== expected.author.assistantId || content?.type !== "text" || content.text !== expected.content.text || values.tenantId !== expected.tenantId || values.workspaceId !== expected.workspaceId || values.projectId !== expected.projectId || values.roomId !== expected.roomId || values.source !== "agent" || values.sourceId !== expected.sourceId || values.idempotencyKey !== expected.idempotencyKey) return void 0;
39344
+ return {
39345
+ id: values.id,
39346
+ tenantId: expected.tenantId,
39347
+ workspaceId: expected.workspaceId,
39348
+ projectId: expected.projectId,
39349
+ roomId: expected.roomId,
39350
+ author: { type: "bot", membershipId: expected.author.membershipId, assistantId: expected.author.assistantId },
39351
+ content: { type: "text", text: expected.content.text },
39352
+ mentions: [],
39353
+ source: "agent",
39354
+ sourceId: expected.sourceId,
39355
+ idempotencyKey: expected.idempotencyKey,
39356
+ createdAt
39357
+ };
39358
+ }
38661
39359
 
38662
39360
  // src/services/ProjectTaskOwnerPolicy.ts
38663
39361
  var import_protocols30 = require("@axiom-lattice/protocols");
@@ -38819,11 +39517,11 @@ function snapshotDate(value) {
38819
39517
  }
38820
39518
 
38821
39519
  // src/services/TaskLifecycleService.ts
38822
- var import_node_crypto10 = require("crypto");
39520
+ var import_node_crypto11 = require("crypto");
38823
39521
  var import_protocols31 = require("@axiom-lattice/protocols");
38824
39522
 
38825
39523
  // src/middlewares/taskBelief.ts
38826
- var import_node_crypto9 = require("crypto");
39524
+ var import_node_crypto10 = require("crypto");
38827
39525
  function buildBeliefCompletionEventKey(taskId, result, impacts) {
38828
39526
  const canonical = JSON.stringify({
38829
39527
  beliefImpact: impacts.map((impact) => ({
@@ -38834,7 +39532,7 @@ function buildBeliefCompletionEventKey(taskId, result, impacts) {
38834
39532
  })),
38835
39533
  result: result.trim()
38836
39534
  });
38837
- return (0, import_node_crypto9.createHash)("sha256").update(canonical).digest("hex");
39535
+ return (0, import_node_crypto10.createHash)("sha256").update(canonical).digest("hex");
38838
39536
  }
38839
39537
  function buildCompletionEvidenceDetail(result, impacts, eventKey) {
38840
39538
  return { result, beliefImpact: impacts, eventKey };
@@ -39037,7 +39735,7 @@ function canonicalJson(value) {
39037
39735
  return JSON.stringify(value);
39038
39736
  }
39039
39737
  function sha256(value) {
39040
- return (0, import_node_crypto10.createHash)("sha256").update(canonicalJson(value)).digest("hex");
39738
+ return (0, import_node_crypto11.createHash)("sha256").update(canonicalJson(value)).digest("hex");
39041
39739
  }
39042
39740
  function sameEvidenceDetail(detail, taskId, result, impacts, eventKey, beliefOwner) {
39043
39741
  const storedOwner = detail.beliefOwner;
@@ -41115,7 +41813,7 @@ function createTaskLifecycleService(deps) {
41115
41813
  }
41116
41814
 
41117
41815
  // src/services/TaskExecutionService.ts
41118
- var import_node_crypto12 = require("crypto");
41816
+ var import_node_crypto13 = require("crypto");
41119
41817
  var import_node_util3 = require("util");
41120
41818
  var import_protocols32 = require("@axiom-lattice/protocols");
41121
41819
 
@@ -41438,13 +42136,13 @@ async function reconcileAllTenantTaskRuns(input) {
41438
42136
  }
41439
42137
 
41440
42138
  // src/services/taskRunLease.ts
41441
- var import_node_crypto11 = require("crypto");
42139
+ var import_node_crypto12 = require("crypto");
41442
42140
  function taskRunLeaseKey(taskId) {
41443
42141
  return `task-run:${taskId}`;
41444
42142
  }
41445
42143
  async function tryAcquireTaskRun(input) {
41446
42144
  const key5 = taskRunLeaseKey(input.taskId);
41447
- const holder = `${input.owner}#${(0, import_node_crypto11.randomUUID)()}`;
42145
+ const holder = `${input.owner}#${(0, import_node_crypto12.randomUUID)()}`;
41448
42146
  if (!await input.store.tryAcquire(key5, holder, input.ttlMs)) return null;
41449
42147
  const heartbeatMs = input.heartbeatMs ?? Math.max(1e3, Math.floor(input.ttlMs / 3));
41450
42148
  let released = false;
@@ -41811,11 +42509,11 @@ function createTaskExecutionService(deps) {
41811
42509
  let threadId;
41812
42510
  let runId;
41813
42511
  try {
41814
- threadId = storedThreadId ?? (deps.createThreadId ?? import_node_crypto12.randomUUID)();
42512
+ threadId = storedThreadId ?? (deps.createThreadId ?? import_node_crypto13.randomUUID)();
41815
42513
  if (task.status === "interrupted" && reuseCurrentRun) {
41816
42514
  runId = storedRunId;
41817
42515
  } else if (task.status === "interrupted" || task.status === "failed") {
41818
- runId = (deps.createRunId ?? import_node_crypto12.randomUUID)();
42516
+ runId = (deps.createRunId ?? import_node_crypto13.randomUUID)();
41819
42517
  }
41820
42518
  } catch (error) {
41821
42519
  return {
@@ -42074,11 +42772,11 @@ function createTaskExecutionService(deps) {
42074
42772
  }
42075
42773
 
42076
42774
  // src/services/leadership.ts
42077
- var import_node_crypto13 = require("crypto");
42775
+ var import_node_crypto14 = require("crypto");
42078
42776
  async function runAsLeader(store, name, fn, options = {}) {
42079
42777
  const key5 = `leader:${name}`;
42080
42778
  const owner2 = options.owner ?? getReplicaId();
42081
- const holder = `${owner2}#${(0, import_node_crypto13.randomUUID)()}`;
42779
+ const holder = `${owner2}#${(0, import_node_crypto14.randomUUID)()}`;
42082
42780
  const ttlMs = options.ttlMs ?? 6e4;
42083
42781
  if (!await store.tryAcquire(key5, holder, ttlMs)) return { ran: false };
42084
42782
  const heartbeat = setInterval(() => {
@@ -43311,16 +44009,16 @@ function createAskUserToClarifyTool() {
43311
44009
  }
43312
44010
 
43313
44011
  // src/middlewares/askUserClarifyMiddleware.ts
43314
- function isRecord9(value) {
44012
+ function isRecord10(value) {
43315
44013
  return typeof value === "object" && value !== null && !Array.isArray(value);
43316
44014
  }
43317
44015
  function isProjectRoomRun(request) {
43318
- if (!isRecord9(request) || !isRecord9(request.runtime)) return false;
44016
+ if (!isRecord10(request) || !isRecord10(request.runtime)) return false;
43319
44017
  const runtime = request.runtime;
43320
- const configurable = isRecord9(runtime.configurable) ? runtime.configurable.runConfig : void 0;
43321
- const context = isRecord9(runtime.context) ? runtime.context.runConfig : void 0;
43322
- const runConfig = isRecord9(configurable) ? configurable : isRecord9(context) ? context : void 0;
43323
- return isRecord9(runConfig?.projectRoom);
44018
+ const configurable = isRecord10(runtime.configurable) ? runtime.configurable.runConfig : void 0;
44019
+ const context = isRecord10(runtime.context) ? runtime.context.runConfig : void 0;
44020
+ const runConfig = isRecord10(configurable) ? configurable : isRecord10(context) ? context : void 0;
44021
+ return isRecord10(runConfig?.projectRoom);
43324
44022
  }
43325
44023
  function createAskUserClarifyMiddleware() {
43326
44024
  return (0, import_langchain79.createMiddleware)({
@@ -44760,15 +45458,16 @@ function executorString(value) {
44760
45458
  }
44761
45459
  async function renderAvailableExecutors(rc, tenantId2) {
44762
45460
  const authority = classifyTaskAuthority(rc);
44763
- const governed = authority.kind === "projectRoom" || authority.kind === "projectTask";
44764
- const projectId = governed ? (authority.trusted.projectTask ?? authority.trusted.projectRoom)?.projectId : void 0;
44765
- const cacheKey = `${tenantId2}:${governed ? `project:${projectId ?? ""}` : "ordinary"}`;
45461
+ const trusted = authority.kind === "projectRoom" || authority.kind === "projectTask" ? authority.trusted : void 0;
45462
+ const useRoster = trusted !== void 0 && !trustedRosterExempt(trusted);
45463
+ const projectId = trusted ? (trusted.projectTask ?? trusted.projectRoom)?.projectId : void 0;
45464
+ const cacheKey = `${tenantId2}:${useRoster ? `project:${projectId ?? ""}` : "tenant"}`;
44766
45465
  const cached2 = executorCache.get(cacheKey);
44767
45466
  const now = Date.now();
44768
45467
  if (cached2 && now - cached2.at < EXECUTOR_CACHE_TTL_MS) return cached2.text;
44769
45468
  const lines = [];
44770
45469
  try {
44771
- if (governed && projectId) {
45470
+ if (useRoster && projectId) {
44772
45471
  const store = getStoreLattice("default", "projectBotMembership").store;
44773
45472
  if (typeof store.list === "function") {
44774
45473
  const bots = await store.list(tenantId2, projectId);
@@ -44783,11 +45482,15 @@ async function renderAvailableExecutors(rc, tenantId2) {
44783
45482
  }
44784
45483
  }
44785
45484
  } else {
45485
+ const exempt = trusted !== void 0 && trustedRosterExempt(trusted) ? trusted : void 0;
45486
+ const excluded = exempt ? await excludedRosterExecutors(exempt) : /* @__PURE__ */ new Set();
44786
45487
  const configs = await agentLatticeManager.getAllAgentConfigsByTenant(tenantId2);
44787
45488
  for (const config of configs.slice(0, 40)) {
44788
45489
  const row = config;
44789
45490
  const id = executorString(row.key);
44790
45491
  if (!id) continue;
45492
+ if (row.ownerUserId !== void 0) continue;
45493
+ if (excluded.has(id)) continue;
44791
45494
  const name = executorString(row.name);
44792
45495
  const description = executorString(row.description);
44793
45496
  lines.push(`- ${id}${name ? ` \u2014 ${name}` : ""}${description ? ` (${description.slice(0, 120)})` : ""}`);
@@ -44799,7 +45502,7 @@ async function renderAvailableExecutors(rc, tenantId2) {
44799
45502
  executorCache.set(cacheKey, { at: now, text: "" });
44800
45503
  return "";
44801
45504
  }
44802
- const header = governed ? "## Available task executors (Project roster)" : "## Available task executors";
45505
+ const header = useRoster ? "## Available task executors (Project roster)" : "## Available task executors";
44803
45506
  const text = `
44804
45507
 
44805
45508
  ${header}
@@ -44811,6 +45514,22 @@ ${lines.join("\n")}
44811
45514
  executorCache.set(cacheKey, { at: now, text });
44812
45515
  return text;
44813
45516
  }
45517
+ async function excludedRosterExecutors(trusted) {
45518
+ const excluded = /* @__PURE__ */ new Set();
45519
+ const context = trusted.projectTask ?? trusted.projectRoom;
45520
+ if (!context) return excluded;
45521
+ try {
45522
+ const store = getStoreLattice("default", "projectBotMembership").store;
45523
+ if (typeof store.list !== "function") return excluded;
45524
+ const rows = await store.list(context.tenantId, context.projectId);
45525
+ for (const row of Array.isArray(rows) ? rows : []) {
45526
+ const record = row;
45527
+ if (record.status !== "active" && typeof record.assistantId === "string") excluded.add(record.assistantId);
45528
+ }
45529
+ } catch {
45530
+ }
45531
+ return excluded;
45532
+ }
44814
45533
  function delegatedTaskScopeError(taskId) {
44815
45534
  return JSON.stringify({
44816
45535
  success: false,
@@ -44860,7 +45579,7 @@ function trustedRosterScope(trusted) {
44860
45579
  };
44861
45580
  }
44862
45581
  function trustedRosterExempt(trusted) {
44863
- return trusted.projectTask?.rosterExempt === true;
45582
+ return trusted.projectTask?.rosterExempt === true || trusted.projectRoom?.rosterExempt === true;
44864
45583
  }
44865
45584
  async function requireTrustedOwner(trusted, targetAssistantId) {
44866
45585
  const context = trusted.projectTask ?? trusted.projectRoom;
@@ -44876,6 +45595,17 @@ async function requireTrustedOwner(trusted, targetAssistantId) {
44876
45595
  targetAssistantId
44877
45596
  });
44878
45597
  }
45598
+ async function rosterExemptOwnerError(tenantId2, targetAssistantId) {
45599
+ try {
45600
+ const lattice = await agentLatticeManager.getOrLoadWithTenant(tenantId2, targetAssistantId);
45601
+ if (lattice && lattice.config.ownerUserId === void 0) return void 0;
45602
+ } catch {
45603
+ }
45604
+ return projectPolicyError(new ProjectTaskOwnerPolicyError(
45605
+ "PROJECT_TASK_OWNER_INACTIVE",
45606
+ "Target Project task owner is not a shared agent of this tenant."
45607
+ ));
45608
+ }
44879
45609
  function trustedTaskContextFailure(task, trusted) {
44880
45610
  if (task.tenantId === trusted.tenantId && task.workspaceId === trusted.workspaceId && task.projectId === trusted.projectId) return void 0;
44881
45611
  return JSON.stringify({
@@ -45480,6 +46210,12 @@ function createTaskMiddleware(options = {}) {
45480
46210
  "Target Project task owner is not active in the exact roster."
45481
46211
  ));
45482
46212
  }
46213
+ } else {
46214
+ const ownerError = await rosterExemptOwnerError(
46215
+ (trustedProject.projectTask ?? trustedProject.projectRoom)?.tenantId ?? tenantId2,
46216
+ ownerId
46217
+ );
46218
+ if (ownerError) return ownerError;
45483
46219
  }
45484
46220
  }
45485
46221
  if (!delegatedTaskId && (typeof effectiveOwnerId !== "string" || effectiveOwnerId.length === 0)) {
@@ -45541,6 +46277,12 @@ function createTaskMiddleware(options = {}) {
45541
46277
  } catch (error) {
45542
46278
  return projectPolicyError(error);
45543
46279
  }
46280
+ } else {
46281
+ const ownerError = await rosterExemptOwnerError(
46282
+ (trustedProject.projectTask ?? trustedProject.projectRoom)?.tenantId ?? tenantId2,
46283
+ ownerId
46284
+ );
46285
+ if (ownerError) return ownerError;
45544
46286
  }
45545
46287
  }
45546
46288
  return store.create({
@@ -46588,7 +47330,7 @@ function createTaskMiddleware(options = {}) {
46588
47330
  }
46589
47331
  const callerAssistantId = rc.assistant_id;
46590
47332
  const agentId = input.agentId === "general-purpose" && callerAssistantId ? `${callerAssistantId}-general-purpose` : input.agentId;
46591
- const service2 = createTaskExecutionService({
47333
+ const service3 = createTaskExecutionService({
46592
47334
  taskStore: getTaskStore(),
46593
47335
  workItemStore: getTaskWorkItemStore(),
46594
47336
  resolveAgent: async (targetTenantId, agentId2) => !!await agentLatticeManager.getOrLoadWithTenant(targetTenantId, agentId2),
@@ -46613,7 +47355,7 @@ function createTaskMiddleware(options = {}) {
46613
47355
  }
46614
47356
  });
46615
47357
  const trustedCaller = authority.kind === "projectTask" || authority.kind === "projectRoom" ? authority.trusted.projectTask ?? authority.trusted.projectRoom : void 0;
46616
- return JSON.stringify(await service2.start({
47358
+ return JSON.stringify(await service3.start({
46617
47359
  tenantId: tenantId2,
46618
47360
  workspaceId: rc.workspaceId,
46619
47361
  projectId: rc.projectId,
@@ -48130,6 +48872,24 @@ function snapshotMembership3(value) {
48130
48872
  function snapshotRows(value) {
48131
48873
  return (0, import_protocols37.snapshotExactArray)(value) ?? [];
48132
48874
  }
48875
+ async function rosterExemptMembershipProjection(scope) {
48876
+ const excluded = /* @__PURE__ */ new Set();
48877
+ const roles = /* @__PURE__ */ new Map();
48878
+ try {
48879
+ const rows = snapshotRows(await getStoreLattice("default", "projectBotMembership").store.list(scope.tenantId, scope.projectId));
48880
+ for (const row of rows) {
48881
+ const member = snapshotMembership3(row);
48882
+ if (!member) continue;
48883
+ if (member.status !== "active") {
48884
+ excluded.add(member.assistantId);
48885
+ continue;
48886
+ }
48887
+ roles.set(member.assistantId, member.role);
48888
+ }
48889
+ } catch {
48890
+ }
48891
+ return { excluded, roles };
48892
+ }
48133
48893
  async function hasActiveCurrentMembership(scope) {
48134
48894
  const memberships = getStoreLattice("default", "projectBotMembership").store;
48135
48895
  const member = snapshotMembership3(await memberships.findById(scope.tenantId, scope.membershipId));
@@ -48146,11 +48906,11 @@ function createProjectRoomMiddleware() {
48146
48906
  if (!scope) return CONTEXT_REQUIRED;
48147
48907
  const digest = (0, import_crypto10.createHash)("sha256").update(text).digest("hex");
48148
48908
  try {
48149
- const service2 = new RoomAgentMessageService({
48909
+ const service3 = new RoomAgentMessageService({
48150
48910
  memberships: getStoreLattice("default", "projectBotMembership").store,
48151
48911
  messages: getStoreLattice("default", "projectRoomMessage").store
48152
48912
  });
48153
- await service2.post({
48913
+ await service3.post({
48154
48914
  tenantId: scope.tenantId,
48155
48915
  workspaceId: scope.workspaceId,
48156
48916
  projectId: scope.projectId,
@@ -48176,6 +48936,25 @@ function createProjectRoomMiddleware() {
48176
48936
  if (!scope) return CONTEXT_REQUIRED;
48177
48937
  try {
48178
48938
  if (!await hasActiveCurrentMembership(scope)) return CONTEXT_REQUIRED;
48939
+ if (scope.rosterExempt === true) {
48940
+ const { excluded, roles } = await rosterExemptMembershipProjection(scope);
48941
+ const configs = await agentLatticeManager.getAllAgentConfigsByTenant(scope.tenantId);
48942
+ const members2 = configs.filter((entry) => entry.ownerUserId === void 0).filter((entry) => {
48943
+ const key5 = entry.key;
48944
+ return typeof key5 === "string" && !excluded.has(key5);
48945
+ }).map((entry) => {
48946
+ const row = entry;
48947
+ const name = typeof row.name === "string" ? row.name : "";
48948
+ const assistantId = typeof row.key === "string" ? row.key : "";
48949
+ return {
48950
+ assistantId,
48951
+ mentionName: name,
48952
+ title: name,
48953
+ role: roles.get(assistantId) ?? "specialist"
48954
+ };
48955
+ }).filter((member) => member.assistantId !== "" && member.mentionName !== "").sort((left, right) => left.assistantId < right.assistantId ? -1 : left.assistantId > right.assistantId ? 1 : 0);
48956
+ return JSON.stringify({ success: true, members: members2 });
48957
+ }
48179
48958
  const rows = await getStoreLattice("default", "projectBotMembership").store.list(scope.tenantId, scope.projectId);
48180
48959
  const members = snapshotRows(rows).map(snapshotMembership3).filter((member) => member !== void 0 && member.status === "active" && member.tenantId === scope.tenantId && member.workspaceId === scope.workspaceId && member.projectId === scope.projectId && member.roomId === scope.roomId).sort((left, right) => left.joinedAt.getTime() - right.joinedAt.getTime() || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)).map((member) => ({
48181
48960
  assistantId: member.assistantId,
@@ -48446,6 +49225,18 @@ On top of that, add any of three combinable classes:
48446
49225
  ## Combinations
48447
49226
  A plugin is the SET of classes it needs, e.g. \`connection + tools + skills\` or \`UI + connection\`.
48448
49227
 
49228
+ ## Scenarios \u2192 skills
49229
+ | Scenario | Read |
49230
+ |---|---|
49231
+ | Tool that returns/transforms data | \`sandbox-plugins-manifest\` + \`sandbox-plugins-handlers\` |
49232
+ | Skills-only (procedural knowledge, no tools) | \`sandbox-plugins-authoring\` (skills) |
49233
+ | Tool needs credentials / resource discovery | \`sandbox-plugins-connection\` |
49234
+ | Tool shows an interactive panel | \`sandbox-plugins-ui\` |
49235
+ | Tool orchestrates platform tools/agents | \`sandbox-plugins-consume-open\` |
49236
+ | Tool callable by external MCP clients | \`sandbox-plugins-expose\` |
49237
+ | Run as a long-lived resident service | \`sandbox-plugins-deploy\` |
49238
+ | Validate / test / debug | \`sandbox-plugins-testing\` |
49239
+
48449
49240
  ## Agents (not a supported authoring path)
48450
49241
  The manifest schema accepts an \`agents\` block and tenant plugin agents are
48451
49242
  registered, but building agents this way is not supported or designed for tenant
@@ -48656,10 +49447,38 @@ lifecycle handshake:
48656
49447
  - \`ui/notifications/tool-result\` \u2192 a \`CallToolResult\` \`{ content: [{ type: "text", text }] }\` (the tool output text, with any \`mcp_app\` fence stripped).
48657
49448
 
48658
49449
  Register the \`message\` listener **before** \`ui/initialize\` so nothing is missed.
48659
- The result carries **TEXT content only** \u2014 no \`structuredContent\` or \`artifact\`;
48660
- parse the JSON yourself if the tool emitted JSON. To re-query or refresh, call
48661
- \`tools/call\` from the View and render its response (the host does not re-push
48662
- \`tool-result\` for your own calls).
49450
+ Read the payload from \`params.content[0].text\` \u2014 the notification is a
49451
+ \`CallToolResult\`, **not** your handler payload at the top level. The result
49452
+ carries **TEXT content only** \u2014 no \`structuredContent\` or \`artifact\`; parse the
49453
+ JSON yourself if the tool emitted JSON (it is the handler's \`result\`, so the
49454
+ handler must wrap it under \`result\`). To re-query or refresh, call \`tools/call\`
49455
+ from the View and render its response (the host does not re-push \`tool-result\`
49456
+ for your own calls).
49457
+
49458
+ When you call \`tools/call\` from the View, the JSON-RPC response \`result\` is the
49459
+ handler's own \`result\` value **directly** \u2014 it is **not wrapped in a**
49460
+ \`CallToolResult\` \`{ content: [...] }\` (that wrapper is only used for the host's
49461
+ \`tool-result\` push). Parse it as your tool's data.
49462
+
49463
+ ## Push vs re-execute (key for debugging)
49464
+ The host pushes \`tool-result\` from the tool result **recorded when the agent ran** \u2014
49465
+ it does **not** re-execute the handler. The push therefore always reflects that
49466
+ run's output (and the handler's \`result\` at that time). A \`tools/call\` from the
49467
+ View, by contrast, **re-executes** the handler live every time.
49468
+
49469
+ So if the push looks stale/wrong but a fresh \`tools/call\` returns the right data,
49470
+ the recorded run predates your change \u2014 re-run the tool (a new agent call) to get
49471
+ a fresh push. Do not assume the push re-reads the handler.
49472
+
49473
+ ## Self-check / debug
49474
+ - Log the raw frame: \`console.log(JSON.stringify(ev.data))\` in the \`message\`
49475
+ listener, and open the browser console inside the sandboxed frame.
49476
+ - If the pushed text looks like the raw handler envelope (e.g. it starts with
49477
+ \`{"ok":true,...\`), the handler omitted the \`result\` wrapper \u2014 the host falls
49478
+ back to the raw stdout instead of your payload. Fix the handler (see
49479
+ \`sandbox-plugins-handlers\`).
49480
+ - The push only happens after \`ui/initialize\` **and**
49481
+ \`ui/notifications/initialized\`; a listener registered later misses it.
48663
49482
 
48664
49483
  \`templates/ui.html\` implements all of this in vanilla JS and renders both payloads
48665
49484
  into a \`#data\` element.
@@ -48779,6 +49598,76 @@ Templates: see \`sandbox-plugins-manifest\` for the exact \`tools[]\` fields; th
48779
49598
  \`templates/plugin.json\` already includes \`"expose": true\` (the tool is grantable via
48780
49599
  an API key but not callable until a key grants it). Remove it for tools that should
48781
49600
  not be externally grantable at all.
49601
+ `,
49602
+ resources: {}
49603
+ },
49604
+ "sandbox-plugins-consume-open": {
49605
+ version: "1.0.0",
49606
+ content: `---
49607
+ name: sandbox-plugins-consume-open
49608
+ description: Call the platform's Open MCP surface from a tenant plugin handler (discover and invoke other tools/agents). Trigger: a plugin tool must orchestrate platform capabilities, call another tool or agent, or reuse an existing capability instead of re-implementing it.
49609
+ ---
49610
+
49611
+ # Calling the platform from a handler (Open MCP)
49612
+
49613
+ A plugin handler can call **the platform's own tools and agents** through the Open
49614
+ MCP endpoint. Reach for this when a tool should orchestrate existing capabilities
49615
+ (read the knowledge base, create a task, invoke an agent) instead of
49616
+ re-implementing them.
49617
+
49618
+ ## What the handler gets
49619
+ When the gateway injects the credential, each handler process starts with:
49620
+ - \`API_GATEWAY_URL\` \u2014 the gateway base URL.
49621
+ - \`OPEN_API_TOKEN\` \u2014 a short-lived bearer **scoped to this tenant's plugin callers**.
49622
+
49623
+ If the deployment does not inject them (no \`PUBLIC_GATEWAY_URL\`), the variables are
49624
+ **absent** \u2014 detect that and degrade gracefully (report "platform tools
49625
+ unavailable") rather than crashing.
49626
+
49627
+ ## Discover, then call
49628
+ Never hardcode the tool list: the callable set is **reflected per tenant** and
49629
+ changes as plugins are exposed. Call \`listTools()\` at runtime.
49630
+ \`\`\`js
49631
+ const { OpenMcpClient } = require("@axiom-lattice/open-mcp-client");
49632
+ // or require("/root/.agents/sdk/open-mcp-client/index.js") when provisioned there
49633
+
49634
+ async function main() {
49635
+ const input = process.env.PLUGIN_INPUT_FILE
49636
+ ? JSON.parse(require("fs").readFileSync(process.env.PLUGIN_INPUT_FILE, "utf8"))
49637
+ : JSON.parse(process.env.PLUGIN_INPUT || "{}");
49638
+
49639
+ const client = OpenMcpClient.fromEnv(); // API_GATEWAY_URL + OPEN_API_TOKEN
49640
+ const tools = await client.listTools(); // exactly what this handler may call
49641
+ const kb = await client.callTool("kb_search", { collection: "docs", query: input.q });
49642
+
49643
+ process.stdout.write(JSON.stringify({ ok: true, result: kb }));
49644
+ }
49645
+ main().catch((err) => {
49646
+ process.stdout.write(JSON.stringify({ ok: false, error: String(err && err.message ? err.message : err) }));
49647
+ });
49648
+ \`\`\`
49649
+
49650
+ - Tool names are \`{domain}_{action}\` (e.g. \`kb_search\`, \`task_manage_task\`).
49651
+ - Args follow the tool's \`inputSchema\`; \`listTools()\` returns name/description.
49652
+ - Return the result through the normal handler envelope (\`{ ok: true, result }\`)
49653
+ so it becomes this tool's output \u2014 see \`sandbox-plugins-handlers\`.
49654
+
49655
+ ## What the handler may call
49656
+ The token carries the conservative sandbox defaults (\`sandbox_*\`, \`kb_search\`)
49657
+ **plus the tenant's grantable Open catalog**: every exposed tenant/global plugin
49658
+ tool and the tenant's agents. A call outside that set fails (tool-not-found or
49659
+ \`GRANT_DENIED\`); retrying will not help.
49660
+
49661
+ ## Client availability
49662
+ The zero-dependency \`@axiom-lattice/open-mcp-client\` must be present in the
49663
+ sandbox: preinstalled in the image, at \`/root/.agents/sdk/open-mcp-client\`, or
49664
+ installed in this run (\`npm install @axiom-lattice/open-mcp-client\`). The CLI is
49665
+ also available: \`npx @axiom-lattice/open-mcp-client tools list --json\`.
49666
+
49667
+ ## Limits
49668
+ - Nested sandbox orchestration is depth-limited (5); past the cap no token is
49669
+ re-issued, so deep recursion fails fast.
49670
+ - Only call what \`listTools()\` returns, and handle a missing tool gracefully.
48782
49671
  `,
48783
49672
  resources: {}
48784
49673
  },
@@ -48819,10 +49708,23 @@ for the single/first connection, or \`data.connections\` (entries of
48819
49708
  \`{ key, config }\`) when the plugin selects multiple connections.
48820
49709
 
48821
49710
  ## Writing output
48822
- Print a JSON envelope to stdout:
49711
+ Every tool handler must print **one** JSON envelope to stdout, with your payload
49712
+ under \`result\`:
48823
49713
  \`\`\`js
48824
- process.stdout.write(JSON.stringify({ ok: true, result: "done" }));
49714
+ process.stdout.write(JSON.stringify({ ok: true, result: <payload> }));
48825
49715
  \`\`\`
49716
+
49717
+ > **The payload MUST go under \`result\`.** \`result\` may be any JSON value
49718
+ > (object, array, string, number).
49719
+ > \`\`\`js
49720
+ > // \u2705 correct \u2014 data under \`result\`
49721
+ > process.stdout.write(JSON.stringify({ ok: true, result: { tasks, count } }));
49722
+ >
49723
+ > // \u274C wrong \u2014 \`result\` missing; the host surfaces the raw stdout (this
49724
+ > // envelope), so callers and MCP App UIs never see \`tasks\`
49725
+ > process.stdout.write(JSON.stringify({ ok: true, tasks, count }));
49726
+ > \`\`\`
49727
+
48826
49728
  Non-JSON stdout is treated as a plain-text result. To fail, either print
48827
49729
  \`{ ok: false, error }\` or exit non-zero (stderr is not required; stdout is
48828
49730
  captured).
@@ -48923,6 +49825,70 @@ Fall back to \`execute_code\`/\`shell\` to run \`node handlers/x.js\` manually w
48923
49825
  \`PLUGIN_INPUT\` and a synthetic \`PLUGIN_CONNECTION_FILE\` to reproduce logic bugs.
48924
49826
  A synthetic fixture only verifies plumbing \u2014 a real upstream call requires a real
48925
49827
  \`connectionKey\` configured by a human.
49828
+ `,
49829
+ resources: {}
49830
+ },
49831
+ "sandbox-plugins-deploy": {
49832
+ version: "1.0.0",
49833
+ content: `---
49834
+ name: sandbox-plugins-deploy
49835
+ description: Deploy a validated tenant plugin as a resident service (alwaysOn vs onDemand), check its status, and undeploy it. Trigger: after a plugin's tests pass and it should run as a long-lived service, or when inspecting/removing a deployment.
49836
+ ---
49837
+
49838
+ # Deploying a Plugin as a Resident Service
49839
+
49840
+ By default a plugin handler runs as a short-lived process on each tool call.
49841
+ Optionally, you can **deploy** the plugin as a **resident service**: one
49842
+ long-lived sandbox per plugin that serves every call. This is faster for
49843
+ frequent or heavy calls, but costs a running sandbox.
49844
+
49845
+ ## Precondition: tests must pass first
49846
+ Deploy ONLY after the plugin is validated AND its tests pass:
49847
+ 1. \`manage_plugin validate\` \u2014 no diagnostics.
49848
+ 2. \`manage_plugin create\`/\`update\` \u2014 written and reloaded.
49849
+ 3. \`manage_plugin test\` \u2014 every tool/connection test returns \`ok: true\`
49850
+ (a real \`connectionKey\` test for credentialed tools).
49851
+ Never deploy a plugin you have not tested: a resident service wraps the same
49852
+ handlers, so an untested handler fails identically \u2014 just in a service you now
49853
+ have to undeploy.
49854
+
49855
+ ## Choose a mode
49856
+ - \`"onDemand"\` (default) \u2014 the service is paused when idle and resumed on the
49857
+ next call. Best for plugins used occasionally. Lower steady cost.
49858
+ - \`"alwaysOn"\` \u2014 the service stays resident and is restarted if it becomes
49859
+ unhealthy. Best for plugins on a hot path or with expensive startup.
49860
+
49861
+ ## Deploy
49862
+ \`\`\`jsonc
49863
+ { "action": "deploy", "type": "my-plugin", "mode": "alwaysOn" }
49864
+ \`\`\`
49865
+ Returns \`{ deployed, status, endpoint, mode }\`. \`status\` is \`"running"\` when
49866
+ the service answered its health check. On failure you get
49867
+ \`{ deployed: false, error }\` \u2014 report it, do not treat it as success.
49868
+ Deploying again is idempotent: it reuses the same service.
49869
+
49870
+ ## Check status
49871
+ \`\`\`jsonc
49872
+ { "action": "status", "type": "my-plugin" }
49873
+ \`\`\`
49874
+ Returns \`{ status, endpoint?, mode?, lastHealthAt?, lastError? }\`. \`"absent"\`
49875
+ means no deployment exists; \`"failed"\` (with \`lastError\`) means the service is
49876
+ down. Inspect this before reporting the plugin as serving.
49877
+
49878
+ ## Undeploy
49879
+ \`\`\`jsonc
49880
+ { "action": "undeploy", "type": "my-plugin" }
49881
+ \`\`\`
49882
+ Returns \`{ undeployed }\`. Undeploy removes the resident sandbox and its
49883
+ deployment record; the plugin files and manifest are untouched, so tool calls
49884
+ fall back to the short-process path. Use it to stop paying for a service you no
49885
+ longer need.
49886
+
49887
+ ## When resident services are unavailable
49888
+ If the environment has no deployment service registered, deploy/undeploy/status
49889
+ return a diagnostic (\`{ deployed: false, error }\` etc.) instead of throwing.
49890
+ That is not a plugin defect: the plugin still works through the default
49891
+ short-process path, so report it as not-deployed rather than broken.
48926
49892
  `,
48927
49893
  resources: {}
48928
49894
  }
@@ -48944,6 +49910,8 @@ Follow the authoring skill's "Decide first" steps to confirm the goal
48944
49910
  (inputs/outputs). If the UI class applies, load "sandbox-plugins-ui" before
48945
49911
  authoring it. If the tool must be callable by external MCP clients via an API key,
48946
49912
  load "sandbox-plugins-expose" and follow it (\`expose: true\` + key grants).
49913
+ If a tool must call the platform's own tools/agents from its handler, load
49914
+ "sandbox-plugins-consume-open".
48947
49915
 
48948
49916
  Plan with persistent Tasks: MANDATORY until you load "task-tracking" with the
48949
49917
  \`skill\` tool, call neither \`manage_task\` nor any other task operation. Use
@@ -48959,10 +49927,197 @@ the connector and give you its key. Only after a real \`connectionKey\` test
48959
49927
  passes may you call the tool verified; without it, report functionality as
48960
49928
  unverified (missing credentials), not as passing. Use \`shell_exec\` (the tool
48961
49929
  provided by the \`code_eval\` middleware) to run \`node handlers/x.js\` for manual
48962
- checks when reproducing a failure.`;
49930
+ checks when reproducing a failure.
49931
+
49932
+ After the plugin passes \`validate\` and its tests, deploy it as a resident
49933
+ service: load "sandbox-plugins-deploy" and follow it. Choose \`onDemand\`
49934
+ (default) or \`alwaysOn\` for hot paths, call \`manage_plugin deploy\` with the
49935
+ chosen \`mode\`, then confirm with \`manage_plugin status\` (\`running\`). Only claim
49936
+ the plugin is deployed and serving once \`status\` reports \`running\`; if the
49937
+ environment reports no deployment service, say so and treat deployment as
49938
+ unavailable \u2014 do NOT claim success. \`undeploy\` removes the service without
49939
+ touching the plugin files.`;
49940
+
49941
+ // src/middlewares/sandboxPluginsUi.ts
49942
+ var SANDBOX_PLUGINS_UI_RESOURCE = "ui://sandbox-plugins/app";
49943
+ var SANDBOX_PLUGINS_APP_HTML = `<!doctype html>
49944
+ <html lang="en">
49945
+ <head>
49946
+ <meta charset="utf-8" />
49947
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
49948
+ <title>Plugin Deployments</title>
49949
+ <style>
49950
+ :root { color-scheme: light dark; }
49951
+ * { box-sizing: border-box; }
49952
+ body {
49953
+ margin: 0;
49954
+ padding: 14px 16px;
49955
+ font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
49956
+ font-size: 13px;
49957
+ color: #1f2430;
49958
+ background: transparent;
49959
+ }
49960
+ h1 { font-size: 14px; font-weight: 600; margin: 0 0 2px; }
49961
+ .subtitle { color: #6b7280; font-size: 12px; margin: 0 0 12px; }
49962
+ table { width: 100%; border-collapse: collapse; }
49963
+ th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid #eceef2; vertical-align: top; }
49964
+ th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: #6b7280; font-weight: 600; }
49965
+ td.type { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 600; white-space: nowrap; }
49966
+ td.mode { white-space: nowrap; color: #4b5563; }
49967
+ td.detail { color: #4b5563; word-break: break-word; max-width: 280px; }
49968
+ .muted { color: #9ca3af; }
49969
+ .status { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
49970
+ .dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; background: #9ca3af; }
49971
+ .dot.running { background: #22c55e; }
49972
+ .dot.failed { background: #ef4444; }
49973
+ .dot.requested, .dot.provisioning, .dot.stopping { background: #f59e0b; }
49974
+ .dot.paused, .dot.stopped { background: #94a3b8; }
49975
+ .state { color: #6b7280; padding: 14px 2px; }
49976
+ .error { margin: 0 0 10px; padding: 8px 10px; border-radius: 8px; background: #fef2f2; color: #b91c1c; }
49977
+ </style>
49978
+ </head>
49979
+ <body>
49980
+ <h1>Plugin Deployments</h1>
49981
+ <p class="subtitle">Resident services deployed for this tenant. Read-only.</p>
49982
+ <div id="root"><p class="state">Loading\u2026</p></div>
49983
+ <script>
49984
+ (function () {
49985
+ var root = document.getElementById("root");
49986
+
49987
+ function post(message) {
49988
+ window.parent.postMessage(message, "*");
49989
+ }
49990
+ function notify(method, params) {
49991
+ post({ jsonrpc: "2.0", method: method, params: params });
49992
+ }
49993
+ var nextId = 1;
49994
+ function rpc(method, params) {
49995
+ var id = nextId++;
49996
+ return new Promise(function (resolve, reject) {
49997
+ function onMessage(event) {
49998
+ var data = event.data;
49999
+ if (!data || data.id !== id) return;
50000
+ window.removeEventListener("message", onMessage);
50001
+ if (data.error) reject(new Error((data.error && data.error.message) || "rpc error"));
50002
+ else resolve(data.result);
50003
+ }
50004
+ window.addEventListener("message", onMessage);
50005
+ post({ jsonrpc: "2.0", id: id, method: method, params: params });
50006
+ });
50007
+ }
50008
+
50009
+ function sendSize() {
50010
+ var width = Math.ceil(document.documentElement.scrollWidth || document.body.scrollWidth);
50011
+ var height = Math.ceil(document.documentElement.scrollHeight || document.body.scrollHeight);
50012
+ notify("ui/notifications/size-changed", { width: width, height: height });
50013
+ }
50014
+
50015
+ function esc(value) {
50016
+ return String(value === null || value === undefined ? "" : value).replace(/[&<>"']/g, function (ch) {
50017
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
50018
+ });
50019
+ }
50020
+
50021
+ function statusClass(status) {
50022
+ switch (status) {
50023
+ case "running": return "running";
50024
+ case "failed": return "failed";
50025
+ case "requested":
50026
+ case "provisioning":
50027
+ case "stopping": return "requested";
50028
+ case "paused":
50029
+ case "stopped": return "paused";
50030
+ default: return "";
50031
+ }
50032
+ }
50033
+
50034
+ function healthCell(item) {
50035
+ if (item.lastError) return '<span style="color:#b91c1c">' + esc(item.lastError) + "</span>";
50036
+ if (item.lastHealthAt) return '<span class="muted">' + esc(item.lastHealthAt) + "</span>";
50037
+ return '<span class="muted">\u2014</span>';
50038
+ }
50039
+
50040
+ function render(payload) {
50041
+ var deployments = payload && Array.isArray(payload.deployments) ? payload.deployments : [];
50042
+ var error = payload && payload.error ? String(payload.error) : "";
50043
+ var html = "";
50044
+ if (error) html += '<div class="error">' + esc(error) + "</div>";
50045
+ if (deployments.length === 0) {
50046
+ html += '<p class="state">' + (error ? "No deployments available." : "No resident plugin deployments.") + "</p>";
50047
+ root.innerHTML = html;
50048
+ sendSize();
50049
+ return;
50050
+ }
50051
+ html += "<table><thead><tr><th>Plugin</th><th>Status</th><th>Mode</th><th>Endpoint</th><th>Health</th></tr></thead><tbody>";
50052
+ deployments.forEach(function (item) {
50053
+ var status = item && item.status ? String(item.status) : "unknown";
50054
+ html += "<tr>";
50055
+ html += '<td class="type">' + esc(item && item.pluginType) + "</td>";
50056
+ html += '<td><span class="status"><span class="dot ' + statusClass(status) + '"></span>' + esc(status) + "</span></td>";
50057
+ html += '<td class="mode">' + esc(item && item.mode) + "</td>";
50058
+ html += '<td class="detail">' + (item && item.endpoint ? esc(item.endpoint) : '<span class="muted">\u2014</span>') + "</td>";
50059
+ html += '<td class="detail">' + healthCell(item || {}) + "</td>";
50060
+ html += "</tr>";
50061
+ });
50062
+ html += "</tbody></table>";
50063
+ root.innerHTML = html;
50064
+ sendSize();
50065
+ }
50066
+
50067
+ function readResult(params) {
50068
+ if (!params || typeof params !== "object") return null;
50069
+ if (params.structuredContent && typeof params.structuredContent === "object") {
50070
+ return params.structuredContent;
50071
+ }
50072
+ var content = params.content;
50073
+ var text = content && content[0] && typeof content[0].text === "string" ? content[0].text : "";
50074
+ if (!text) return null;
50075
+ try { return JSON.parse(text); } catch (err) { return null; }
50076
+ }
50077
+
50078
+ // 1) Register the tool-result listener BEFORE initializing so the data
50079
+ // the host pushes right after the handshake is never missed.
50080
+ window.addEventListener("message", function (event) {
50081
+ var data = event.data;
50082
+ if (!data || typeof data !== "object" || typeof data.method !== "string") return;
50083
+ if (data.method === "ui/notifications/tool-result") {
50084
+ render(readResult(data.params));
50085
+ }
50086
+ });
50087
+
50088
+ if (typeof ResizeObserver !== "undefined") {
50089
+ new ResizeObserver(sendSize).observe(document.documentElement);
50090
+ } else {
50091
+ window.addEventListener("resize", sendSize);
50092
+ }
50093
+
50094
+ // 2) MCP Apps lifecycle: initialize, then signal readiness. The host
50095
+ // pushes tool data only after this completes.
50096
+ rpc("ui/initialize", {
50097
+ protocolVersion: "2025-06-18",
50098
+ capabilities: {},
50099
+ clientInfo: { name: "sandbox-plugins-ui", version: "1.0.0" },
50100
+ appCapabilities: { availableDisplayModes: ["inline"] }
50101
+ }).then(function () {
50102
+ notify("ui/notifications/initialized", {});
50103
+ sendSize();
50104
+ }).catch(function (err) {
50105
+ root.innerHTML = '<p class="state">Failed to initialize: ' + esc((err && err.message) || err) + "</p>";
50106
+ sendSize();
50107
+ });
50108
+ })();
50109
+ </script>
50110
+ </body>
50111
+ </html>
50112
+ `;
48963
50113
 
48964
50114
  // src/middlewares/sandboxPluginsMiddleware.ts
48965
- var PLUGIN_BUILDER_SKILLS = [...Object.keys(SANDBOX_PLUGIN_SKILLS), "task-tracking"];
50115
+ var SANDBOX_PLUGINS_UI_REF = {
50116
+ kind: "plugin",
50117
+ pluginType: "sandbox-plugins",
50118
+ resource: SANDBOX_PLUGINS_UI_RESOURCE,
50119
+ displayMode: "inline"
50120
+ };
48966
50121
  function tenantIdFrom(exeConfig) {
48967
50122
  const c = exeConfig;
48968
50123
  return c?.configurable?.runConfig?.tenantId ?? "default";
@@ -48979,6 +50134,15 @@ function collectSecretValues(connection, fields) {
48979
50134
  function errorText(err) {
48980
50135
  return err instanceof Error ? err.message : String(err);
48981
50136
  }
50137
+ function deploymentUnavailable(action) {
50138
+ const error = `plugin deployment service is not configured; cannot ${action}. Resident plugin services are unavailable in this environment.`;
50139
+ if (action === "undeploy") return JSON.stringify({ undeployed: false, error });
50140
+ if (action === "status") return JSON.stringify({ status: "unavailable", error });
50141
+ return JSON.stringify({ deployed: false, error });
50142
+ }
50143
+ function deploymentsResult(payload) {
50144
+ return appendUiFence(JSON.stringify(payload), SANDBOX_PLUGINS_UI_REF);
50145
+ }
48982
50146
  function redactString(value, secrets) {
48983
50147
  let out = value;
48984
50148
  for (const secret of secrets) out = out.split(secret).join("[redacted]");
@@ -48999,10 +50163,24 @@ function redactSecretValues(payload, secrets) {
48999
50163
  return walk(payload);
49000
50164
  }
49001
50165
  var managePluginSchema = import_zod71.z.object({
49002
- action: import_zod71.z.enum(["list", "get", "validate", "create", "update", "delete", "reload", "test"]),
50166
+ action: import_zod71.z.enum([
50167
+ "list",
50168
+ "get",
50169
+ "validate",
50170
+ "create",
50171
+ "update",
50172
+ "delete",
50173
+ "reload",
50174
+ "test",
50175
+ "deploy",
50176
+ "undeploy",
50177
+ "status",
50178
+ "deployments"
50179
+ ]),
49003
50180
  type: import_zod71.z.string().optional(),
49004
50181
  manifest: import_zod71.z.record(import_zod71.z.unknown()).optional(),
49005
50182
  handlers: import_zod71.z.record(import_zod71.z.string()).optional(),
50183
+ mode: import_zod71.z.enum(["onDemand", "alwaysOn"]).optional(),
49006
50184
  target: import_zod71.z.object({
49007
50185
  kind: import_zod71.z.enum(["tool", "connection-test", "connection-discover"]),
49008
50186
  name: import_zod71.z.string().optional(),
@@ -49015,9 +50193,18 @@ var sandboxPluginsPlugin = {
49015
50193
  type: "sandbox-plugins",
49016
50194
  category: "execution",
49017
50195
  name: "Sandbox Plugins",
49018
- description: "Author and manage tenant plugins stored in the sandbox: list/validate/create/update/delete/reload/test. Tools are agent-callable JS handlers and may declare an optional interactive HTML UI (rendered as a sandboxed MCP App). Pairs with the Skill plugin for authoring guidance.",
50196
+ description: "Author and manage tenant plugins stored in the sandbox: list/validate/create/update/delete/reload/test, and deploy/undeploy/status/deployments to run a plugin as a resident service. Tools are agent-callable JS handlers and may declare an optional interactive HTML UI (rendered as a sandboxed MCP App). Pairs with the Skill plugin for authoring guidance.",
49019
50197
  recommends: ["skill"],
49020
- tools: [{ name: "manage_plugin", description: "Author and manage tenant sandbox plugins (tools, optional per-tool HTML UI, connections, and Open/MCP exposure)" }],
50198
+ tools: [
50199
+ {
50200
+ name: "manage_plugin",
50201
+ description: "Author and manage tenant sandbox plugins (tools, optional per-tool HTML UI, connections, Open/MCP exposure, and resident deployment). Actions: list/get/validate/create/update/delete/reload/test, deploy/undeploy/status, and `deployments` to list every resident deployment for the tenant.",
50202
+ ui: { resource: SANDBOX_PLUGINS_UI_RESOURCE, displayMode: "inline" }
50203
+ }
50204
+ ],
50205
+ uiResources: {
50206
+ [SANDBOX_PLUGINS_UI_RESOURCE]: { html: SANDBOX_PLUGINS_APP_HTML }
50207
+ },
49021
50208
  configSchema: { type: "object", properties: {} },
49022
50209
  defaultConfig: {}
49023
50210
  },
@@ -49038,7 +50225,7 @@ var sandboxPluginsPlugin = {
49038
50225
  id: "sandbox-plugins",
49039
50226
  type: "sandbox-plugins",
49040
50227
  name: "Sandbox Plugins",
49041
- description: "Author and manage tenant plugins (list/validate/create/update/delete/reload/test)",
50228
+ description: "Author and manage tenant plugins (list/validate/create/update/delete/reload/test/deploy/undeploy/status)",
49042
50229
  enabled: true,
49043
50230
  config: {}
49044
50231
  },
@@ -49046,9 +50233,9 @@ var sandboxPluginsPlugin = {
49046
50233
  id: "skill",
49047
50234
  type: "skill",
49048
50235
  name: "Skill",
49049
- description: "Load the sandbox-plugins authoring, patterns, manifest, handler, connection, UI, and testing skills",
50236
+ description: "Load any available skill, including the sandbox-plugins authoring, patterns, manifest, handler, connection, UI, and testing skills",
49050
50237
  enabled: true,
49051
- config: { readAll: false, skills: PLUGIN_BUILDER_SKILLS }
50238
+ config: { readAll: true }
49052
50239
  },
49053
50240
  {
49054
50241
  id: "task",
@@ -49229,18 +50416,79 @@ var sandboxPluginsPlugin = {
49229
50416
  assistantId: typeof runCfg.assistant_id === "string" ? runCfg.assistant_id : void 0
49230
50417
  }
49231
50418
  });
49232
- return JSON.stringify(redactSecretValues(result, secrets));
50419
+ return handlerResultText(redactSecretValues(result, secrets));
49233
50420
  } catch (err) {
49234
50421
  return JSON.stringify(redactSecretValues({ ok: false, error: errorText(err) }, secrets));
49235
50422
  }
49236
50423
  }
50424
+ case "deploy": {
50425
+ if (!input.type) return "type is required";
50426
+ const service3 = getPluginDeploymentService();
50427
+ if (!service3) return deploymentUnavailable("deploy");
50428
+ try {
50429
+ const result = await service3.deploy(tenantId2, input.type, {
50430
+ ...input.mode === void 0 ? {} : { mode: input.mode }
50431
+ });
50432
+ return JSON.stringify({
50433
+ deployed: true,
50434
+ status: result.status,
50435
+ endpoint: result.endpoint,
50436
+ mode: result.mode
50437
+ });
50438
+ } catch (err) {
50439
+ return JSON.stringify({ deployed: false, error: errorText(err) });
50440
+ }
50441
+ }
50442
+ case "undeploy": {
50443
+ if (!input.type) return "type is required";
50444
+ const service3 = getPluginDeploymentService();
50445
+ if (!service3) return deploymentUnavailable("undeploy");
50446
+ try {
50447
+ const result = await service3.undeploy(tenantId2, input.type);
50448
+ return JSON.stringify({ undeployed: result.undeployed });
50449
+ } catch (err) {
50450
+ return JSON.stringify({ undeployed: false, error: errorText(err) });
50451
+ }
50452
+ }
50453
+ case "status": {
50454
+ if (!input.type) return "type is required";
50455
+ const service3 = getPluginDeploymentService();
50456
+ if (!service3) return deploymentUnavailable("status");
50457
+ try {
50458
+ const result = await service3.status(tenantId2, input.type);
50459
+ return JSON.stringify({
50460
+ status: result.status,
50461
+ endpoint: result.endpoint,
50462
+ mode: result.mode,
50463
+ lastHealthAt: result.lastHealthAt,
50464
+ lastError: result.lastError
50465
+ });
50466
+ } catch (err) {
50467
+ return JSON.stringify({ status: "unavailable", error: errorText(err) });
50468
+ }
50469
+ }
50470
+ case "deployments": {
50471
+ const service3 = getPluginDeploymentService();
50472
+ if (!service3) {
50473
+ return deploymentsResult({
50474
+ deployments: [],
50475
+ error: "plugin deployment service is not configured; cannot list deployments. Resident plugin services are unavailable in this environment."
50476
+ });
50477
+ }
50478
+ try {
50479
+ const deployments = await service3.list(tenantId2);
50480
+ return deploymentsResult({ deployments });
50481
+ } catch (err) {
50482
+ return deploymentsResult({ deployments: [], error: errorText(err) });
50483
+ }
50484
+ }
49237
50485
  default:
49238
50486
  return `unsupported action: ${input.action}`;
49239
50487
  }
49240
50488
  },
49241
50489
  {
49242
50490
  name: "manage_plugin",
49243
- description: "Author and manage tenant sandbox plugins (list/get/validate/create/update/delete/reload/test).",
50491
+ description: "Author and manage tenant sandbox plugins (list/get/validate/create/update/delete/reload/test) and deploy/undeploy/status a plugin as a resident service.",
49244
50492
  schema: managePluginSchema
49245
50493
  }
49246
50494
  )
@@ -49295,6 +50543,526 @@ function buildBundleFromDist(distDir, version) {
49295
50543
  };
49296
50544
  }
49297
50545
 
50546
+ // src/sandbox_plugin/PluginDeploymentRuntimeRegistry.ts
50547
+ var PluginDeploymentRuntimeRegistry = class {
50548
+ /**
50549
+ * @param options - Optional injectable clock for deterministic health timestamps.
50550
+ */
50551
+ constructor(options = {}) {
50552
+ this.entries = /* @__PURE__ */ new Map();
50553
+ this.inFlight = /* @__PURE__ */ new Map();
50554
+ this.now = options.now ?? Date.now;
50555
+ }
50556
+ /**
50557
+ * Cache the resolved proxy endpoint for a deployment.
50558
+ *
50559
+ * @param id - Composed key `${tenantId}:${pluginType}`.
50560
+ * @param endpoint - Relative proxy path, e.g. `/api/sandboxes/plugin-abc/proxy/8080`.
50561
+ */
50562
+ setEndpoint(id, endpoint) {
50563
+ this.entry(id).endpoint = endpoint;
50564
+ }
50565
+ /**
50566
+ * Read the cached endpoint for a deployment.
50567
+ *
50568
+ * @param id - Composed key `${tenantId}:${pluginType}`.
50569
+ * @returns The cached endpoint, or `undefined` when none is known.
50570
+ */
50571
+ getEndpoint(id) {
50572
+ return this.entries.get(id)?.endpoint;
50573
+ }
50574
+ /**
50575
+ * Record a successful health observation and clear any prior error.
50576
+ *
50577
+ * @param id - Composed key `${tenantId}:${pluginType}`.
50578
+ */
50579
+ markHealthy(id) {
50580
+ const entry = this.entry(id);
50581
+ entry.lastHealthAt = this.now();
50582
+ delete entry.lastError;
50583
+ }
50584
+ /**
50585
+ * Record a failed health observation.
50586
+ *
50587
+ * @param id - Composed key `${tenantId}:${pluginType}`.
50588
+ * @param error - Human-readable failure description.
50589
+ */
50590
+ markUnhealthy(id, error) {
50591
+ const entry = this.entry(id);
50592
+ entry.lastHealthAt = this.now();
50593
+ entry.lastError = error;
50594
+ }
50595
+ /**
50596
+ * Read the last observed health for a deployment.
50597
+ *
50598
+ * @param id - Composed key `${tenantId}:${pluginType}`.
50599
+ * @returns The cached health, or `undefined` when the id is unknown.
50600
+ */
50601
+ getHealth(id) {
50602
+ const entry = this.entries.get(id);
50603
+ if (!entry) return void 0;
50604
+ const health = {};
50605
+ if (entry.lastHealthAt !== void 0) health.lastHealthAt = entry.lastHealthAt;
50606
+ if (entry.lastError !== void 0) health.lastError = entry.lastError;
50607
+ return health;
50608
+ }
50609
+ /**
50610
+ * Run a cold start/resume factory at most once per id at a time.
50611
+ *
50612
+ * Concurrent calls for the same id return the same in-flight promise and run
50613
+ * `factory` exactly once. The in-flight entry is cleared when the factory
50614
+ * settles, so a later call runs it again (retry after failure is allowed).
50615
+ *
50616
+ * @param id - Composed key `${tenantId}:${pluginType}`.
50617
+ * @param factory - Idempotent start/resume operation for the resident service.
50618
+ * @returns A promise that settles with the factory outcome.
50619
+ */
50620
+ ensureRunning(id, factory) {
50621
+ const existing = this.inFlight.get(id);
50622
+ if (existing) return existing;
50623
+ let started;
50624
+ try {
50625
+ started = factory();
50626
+ } catch (error) {
50627
+ started = Promise.reject(error);
50628
+ }
50629
+ const tracked = started.finally(() => {
50630
+ if (this.inFlight.get(id) === tracked) {
50631
+ this.inFlight.delete(id);
50632
+ }
50633
+ });
50634
+ this.inFlight.set(id, tracked);
50635
+ return tracked;
50636
+ }
50637
+ /**
50638
+ * Drop cached endpoint and health for one deployment.
50639
+ *
50640
+ * @param id - Composed key `${tenantId}:${pluginType}`.
50641
+ */
50642
+ clear(id) {
50643
+ this.entries.delete(id);
50644
+ }
50645
+ /** Drop all cached endpoints, health, and in-flight coordination entries. */
50646
+ reset() {
50647
+ this.entries.clear();
50648
+ this.inFlight.clear();
50649
+ }
50650
+ entry(id) {
50651
+ const existing = this.entries.get(id);
50652
+ if (existing) return existing;
50653
+ const created = {};
50654
+ this.entries.set(id, created);
50655
+ return created;
50656
+ }
50657
+ };
50658
+
50659
+ // src/sandbox_plugin/host/pluginHostProtocol.ts
50660
+ var INLINE_INPUT_LIMIT_BYTES2 = 48 * 1024;
50661
+ var DEFAULT_HANDLER_TIMEOUT_MS = 6e4;
50662
+ var MIN_HANDLER_TIMEOUT_MS = 1e3;
50663
+ var MAX_HANDLER_TIMEOUT_MS = 3e5;
50664
+ function isRecord11(value) {
50665
+ return typeof value === "object" && value !== null && !Array.isArray(value);
50666
+ }
50667
+ function isEnvelope3(value) {
50668
+ return isRecord11(value) && typeof value.ok === "boolean";
50669
+ }
50670
+ function parseHostArgs(argv) {
50671
+ let type;
50672
+ let port;
50673
+ for (let index = 0; index < argv.length; index += 1) {
50674
+ const token = argv[index];
50675
+ if (token === "--type" && index + 1 < argv.length) {
50676
+ type = argv[index + 1];
50677
+ index += 1;
50678
+ } else if (token.startsWith("--type=")) {
50679
+ type = token.slice("--type=".length);
50680
+ } else if (token === "--port" && index + 1 < argv.length) {
50681
+ port = argv[index + 1];
50682
+ index += 1;
50683
+ } else if (token.startsWith("--port=")) {
50684
+ port = token.slice("--port=".length);
50685
+ }
50686
+ }
50687
+ const parsedPort = Number(port);
50688
+ if (typeof type !== "string" || type.length === 0) {
50689
+ throw new Error("parseHostArgs: missing or empty --type");
50690
+ }
50691
+ if (!Number.isInteger(parsedPort) || parsedPort <= 0 || parsedPort > 65535) {
50692
+ throw new Error("parseHostArgs: invalid --port");
50693
+ }
50694
+ return { type, port: parsedPort };
50695
+ }
50696
+ function resolveHandlerTimeoutMs(value) {
50697
+ if (value === null || value === void 0 || value === "") return DEFAULT_HANDLER_TIMEOUT_MS;
50698
+ const numeric = typeof value === "number" ? value : Number(value);
50699
+ if (!Number.isFinite(numeric)) return DEFAULT_HANDLER_TIMEOUT_MS;
50700
+ return Math.min(Math.max(Math.round(numeric), MIN_HANDLER_TIMEOUT_MS), MAX_HANDLER_TIMEOUT_MS);
50701
+ }
50702
+ function parseHandlerEnvelope(stdout) {
50703
+ const trimmed = stdout.trim();
50704
+ if (!trimmed) return { ok: false, error: "empty handler output" };
50705
+ const whole = tryParse(trimmed);
50706
+ if (isEnvelope3(whole)) return normalizeEnvelope(whole);
50707
+ const lines = stdout.split("\n");
50708
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
50709
+ const line = lines[index]?.trim();
50710
+ if (!line) continue;
50711
+ const candidate = tryParse(line);
50712
+ if (isEnvelope3(candidate)) return normalizeEnvelope(candidate);
50713
+ }
50714
+ return { ok: true, result: trimmed };
50715
+ }
50716
+ function buildHandlerCommand(handlerPath) {
50717
+ return `node ${quotePosixShellArg2(handlerPath)}`;
50718
+ }
50719
+ function encodeInput(input) {
50720
+ return Buffer.from(JSON.stringify(input ?? null), "utf8").toString("base64");
50721
+ }
50722
+ function buildConnectionPayload2(args) {
50723
+ if (Array.isArray(args.connections) && args.connections.length > 0) {
50724
+ return {
50725
+ connection: args.connection ?? args.connections[0]?.config,
50726
+ connections: args.connections
50727
+ };
50728
+ }
50729
+ if (args.connection) {
50730
+ return { connection: args.connection };
50731
+ }
50732
+ return null;
50733
+ }
50734
+ function quotePosixShellArg2(value) {
50735
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
50736
+ }
50737
+ function tryParse(text) {
50738
+ try {
50739
+ return JSON.parse(text);
50740
+ } catch {
50741
+ return void 0;
50742
+ }
50743
+ }
50744
+ function normalizeEnvelope(parsed) {
50745
+ if (parsed.ok) return { ok: true, result: parsed.result };
50746
+ return { ok: false, error: typeof parsed.error === "string" ? parsed.error : void 0 };
50747
+ }
50748
+
50749
+ // src/sandbox_plugin/host/pluginHostSource.ts
50750
+ var import_node_crypto15 = require("crypto");
50751
+ var PLUGIN_HOST_VERSION = "1.1.0";
50752
+ var PLUGIN_HOST_SERVER_SOURCE = String.raw`#!/usr/bin/env node
50753
+ "use strict";
50754
+
50755
+ var http = require("node:http");
50756
+ var fs = require("node:fs");
50757
+ var os = require("node:os");
50758
+ var path = require("node:path");
50759
+ var crypto = require("node:crypto");
50760
+ var execFile = require("node:child_process").execFile;
50761
+
50762
+ var VERSION = "${PLUGIN_HOST_VERSION}";
50763
+ var PLUGINS_ROOT = "/root/.agents/plugins";
50764
+ var INLINE_INPUT_LIMIT_BYTES = 48 * 1024;
50765
+ var MAX_BODY_BYTES = 8 * 1024 * 1024;
50766
+ var MAX_RESULT_BYTES = 16 * 1024 * 1024;
50767
+ var DEFAULT_HANDLER_TIMEOUT_MS = 60000;
50768
+ var MIN_HANDLER_TIMEOUT_MS = 1000;
50769
+ var MAX_HANDLER_TIMEOUT_MS = 300000;
50770
+
50771
+ function resolveHandlerTimeoutMs(value) {
50772
+ if (value === null || value === undefined || value === "") {
50773
+ return DEFAULT_HANDLER_TIMEOUT_MS;
50774
+ }
50775
+ var numeric = typeof value === "number" ? value : Number(value);
50776
+ if (!isFinite(numeric)) {
50777
+ return DEFAULT_HANDLER_TIMEOUT_MS;
50778
+ }
50779
+ return Math.min(Math.max(Math.round(numeric), MIN_HANDLER_TIMEOUT_MS), MAX_HANDLER_TIMEOUT_MS);
50780
+ }
50781
+
50782
+ function isRecord(value) {
50783
+ return typeof value === "object" && value !== null && !Array.isArray(value);
50784
+ }
50785
+
50786
+ function isEnvelope(value) {
50787
+ return isRecord(value) && typeof value.ok === "boolean";
50788
+ }
50789
+
50790
+ function tryParse(text) {
50791
+ try {
50792
+ return JSON.parse(text);
50793
+ } catch (err) {
50794
+ return undefined;
50795
+ }
50796
+ }
50797
+
50798
+ function normalizeEnvelope(parsed) {
50799
+ if (parsed.ok) {
50800
+ return { ok: true, result: parsed.result };
50801
+ }
50802
+ return { ok: false, error: typeof parsed.error === "string" ? parsed.error : undefined };
50803
+ }
50804
+
50805
+ function parseHandlerEnvelope(stdout) {
50806
+ var text = String(stdout == null ? "" : stdout);
50807
+ var trimmed = text.trim();
50808
+ if (!trimmed) {
50809
+ // Deliberate divergence from SandboxPluginExecutor, which returns
50810
+ // { ok: true, result: "" } for empty stdout; the resident host treats empty
50811
+ // output as a handler failure so a crashed/no-op handler is not a "success".
50812
+ return { ok: false, error: "empty handler output" };
50813
+ }
50814
+ var whole = tryParse(trimmed);
50815
+ if (isEnvelope(whole)) {
50816
+ return normalizeEnvelope(whole);
50817
+ }
50818
+ var lines = text.split("\n");
50819
+ for (var index = lines.length - 1; index >= 0; index -= 1) {
50820
+ var line = lines[index].trim();
50821
+ if (!line) {
50822
+ continue;
50823
+ }
50824
+ var candidate = tryParse(line);
50825
+ if (isEnvelope(candidate)) {
50826
+ return normalizeEnvelope(candidate);
50827
+ }
50828
+ }
50829
+ return { ok: true, result: trimmed };
50830
+ }
50831
+
50832
+ function quotePosixShellArg(value) {
50833
+ return "'" + String(value).replace(/'/g, "'\"'\"'") + "'";
50834
+ }
50835
+
50836
+ function buildHandlerCommand(handlerPath) {
50837
+ return "node " + quotePosixShellArg(handlerPath);
50838
+ }
50839
+
50840
+ function parseHostArgs(argv) {
50841
+ var type;
50842
+ var port;
50843
+ for (var index = 0; index < argv.length; index += 1) {
50844
+ var token = argv[index];
50845
+ if (token === "--type" && index + 1 < argv.length) {
50846
+ type = argv[index + 1];
50847
+ index += 1;
50848
+ } else if (token.indexOf("--type=") === 0) {
50849
+ type = token.slice("--type=".length);
50850
+ } else if (token === "--port" && index + 1 < argv.length) {
50851
+ port = argv[index + 1];
50852
+ index += 1;
50853
+ } else if (token.indexOf("--port=") === 0) {
50854
+ port = token.slice("--port=".length);
50855
+ }
50856
+ }
50857
+ var parsedPort = Number(port);
50858
+ if (typeof type !== "string" || type.length === 0) {
50859
+ throw new Error("parseHostArgs: missing or empty --type");
50860
+ }
50861
+ if (!Number.isInteger(parsedPort) || parsedPort <= 0 || parsedPort > 65535) {
50862
+ throw new Error("parseHostArgs: invalid --port");
50863
+ }
50864
+ return { type: type, port: parsedPort };
50865
+ }
50866
+
50867
+ function encodeInput(input) {
50868
+ return Buffer.from(JSON.stringify(input === undefined ? null : input), "utf8").toString("base64");
50869
+ }
50870
+
50871
+ function buildConnectionPayload(body) {
50872
+ if (Array.isArray(body.connections) && body.connections.length > 0) {
50873
+ return {
50874
+ connection: body.connection !== undefined ? body.connection : body.connections[0].config,
50875
+ connections: body.connections,
50876
+ };
50877
+ }
50878
+ if (body.connection) {
50879
+ return { connection: body.connection };
50880
+ }
50881
+ return null;
50882
+ }
50883
+
50884
+ function resolveHandlerPath(type, handler) {
50885
+ if (typeof handler !== "string" || handler.length === 0) {
50886
+ throw new Error("missing handler");
50887
+ }
50888
+ if (handler.charAt(0) === "/" || handler.split("/").indexOf("..") !== -1) {
50889
+ throw new Error("invalid handler path");
50890
+ }
50891
+ return path.join(PLUGINS_ROOT, type, handler);
50892
+ }
50893
+
50894
+ function readJsonBody(req) {
50895
+ return new Promise(function (resolve, reject) {
50896
+ var chunks = [];
50897
+ var size = 0;
50898
+ req.on("data", function (chunk) {
50899
+ size += chunk.length;
50900
+ if (size > MAX_BODY_BYTES) {
50901
+ reject(new Error("request body too large"));
50902
+ req.destroy();
50903
+ return;
50904
+ }
50905
+ chunks.push(chunk);
50906
+ });
50907
+ req.on("end", function () {
50908
+ try {
50909
+ var text = Buffer.concat(chunks).toString("utf8");
50910
+ resolve(text ? JSON.parse(text) : {});
50911
+ } catch (err) {
50912
+ reject(new Error("invalid JSON body"));
50913
+ }
50914
+ });
50915
+ req.on("error", reject);
50916
+ });
50917
+ }
50918
+
50919
+ function runNode(handlerPath, pluginDir, env, timeoutMs) {
50920
+ return new Promise(function (resolve) {
50921
+ execFile(
50922
+ process.execPath,
50923
+ [handlerPath],
50924
+ { cwd: pluginDir, env: env, timeout: timeoutMs, maxBuffer: MAX_RESULT_BYTES },
50925
+ function (error, stdout, stderr) {
50926
+ resolve({ error: error, stdout: stdout || "", stderr: stderr || "" });
50927
+ }
50928
+ );
50929
+ });
50930
+ }
50931
+
50932
+ function failureFromExec(exec, command) {
50933
+ var parsed = parseHandlerEnvelope(exec.stdout);
50934
+ if (!parsed.ok) {
50935
+ return parsed;
50936
+ }
50937
+ var code = exec.error && typeof exec.error.code === "number" ? exec.error.code : "n/a";
50938
+ var detail = exec.stdout ? exec.stdout.slice(0, 4096) : exec.error ? String(exec.error.message) : "";
50939
+ return { ok: false, error: "handler failed (exit " + code + "): " + detail + " [" + command + "]" };
50940
+ }
50941
+
50942
+ async function runHandler(body) {
50943
+ var pluginDir = path.join(PLUGINS_ROOT, config.type);
50944
+ var handlerPath = resolveHandlerPath(config.type, body.handler);
50945
+ var command = buildHandlerCommand(handlerPath);
50946
+ var inputJson = JSON.stringify(body.input === undefined ? null : body.input);
50947
+ var connectionPayload = buildConnectionPayload(body);
50948
+ var inputIsLarge = Buffer.byteLength(inputJson, "utf8") > INLINE_INPUT_LIMIT_BYTES;
50949
+ var needsTempDir = inputIsLarge || connectionPayload !== null;
50950
+ var tempDir = null;
50951
+ var env = Object.assign({}, process.env);
50952
+ try {
50953
+ if (needsTempDir) {
50954
+ tempDir = path.join(os.tmpdir(), "plugin-run-" + Date.now() + "-" + Math.random().toString(36).slice(2));
50955
+ fs.mkdirSync(tempDir, { recursive: true });
50956
+ }
50957
+ if (inputIsLarge) {
50958
+ var inputFile = path.join(tempDir, "input.json");
50959
+ fs.writeFileSync(inputFile, inputJson);
50960
+ env.PLUGIN_INPUT_FILE = inputFile;
50961
+ } else {
50962
+ env.PLUGIN_INPUT = encodeInput(body.input);
50963
+ }
50964
+ if (connectionPayload !== null) {
50965
+ var connectionFile = path.join(tempDir, "connection.json");
50966
+ fs.writeFileSync(connectionFile, JSON.stringify(connectionPayload));
50967
+ env.PLUGIN_CONNECTION_FILE = connectionFile;
50968
+ }
50969
+ env.PLUGIN_CONTEXT = encodeInput(body.context || {});
50970
+ var handlerTimeoutMs = resolveHandlerTimeoutMs(body.timeoutMs);
50971
+ var exec = await runNode(handlerPath, pluginDir, env, handlerTimeoutMs);
50972
+ if (exec.error) {
50973
+ return failureFromExec(exec, command);
50974
+ }
50975
+ return parseHandlerEnvelope(exec.stdout);
50976
+ } finally {
50977
+ if (tempDir) {
50978
+ try {
50979
+ fs.rmSync(tempDir, { recursive: true, force: true });
50980
+ } catch (err) {
50981
+ // best-effort cleanup
50982
+ }
50983
+ }
50984
+ }
50985
+ }
50986
+
50987
+ function manifestHash() {
50988
+ try {
50989
+ var content = fs.readFileSync(path.join(PLUGINS_ROOT, config.type, "plugin.json"), "utf8");
50990
+ return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
50991
+ } catch (err) {
50992
+ return "";
50993
+ }
50994
+ }
50995
+
50996
+ function sendJson(res, status, payload) {
50997
+ var body = JSON.stringify(payload);
50998
+ res.writeHead(status, {
50999
+ "content-type": "application/json",
51000
+ "content-length": Buffer.byteLength(body),
51001
+ });
51002
+ res.end(body);
51003
+ }
51004
+
51005
+ function handleRequest(req, res) {
51006
+ var url = String(req.url || "").split("?")[0];
51007
+ if (req.method === "GET" && url === "/health") {
51008
+ sendJson(res, 200, { ok: true, version: VERSION });
51009
+ return;
51010
+ }
51011
+ if (req.method === "GET" && url === "/meta") {
51012
+ sendJson(res, 200, { type: config.type, manifestHash: manifestHash() });
51013
+ return;
51014
+ }
51015
+ if (req.method === "POST" && url === "/invoke") {
51016
+ readJsonBody(req)
51017
+ .then(function (body) {
51018
+ if (!isRecord(body)) {
51019
+ sendJson(res, 400, { ok: false, error: "invalid body" });
51020
+ return;
51021
+ }
51022
+ return runHandler(body).then(function (result) {
51023
+ sendJson(res, 200, result);
51024
+ });
51025
+ })
51026
+ .catch(function (err) {
51027
+ sendJson(res, 400, { ok: false, error: String(err && err.message ? err.message : err) });
51028
+ });
51029
+ return;
51030
+ }
51031
+ sendJson(res, 404, { ok: false, error: "not found" });
51032
+ }
51033
+
51034
+ var config = parseHostArgs(process.argv);
51035
+ var server = http.createServer(handleRequest);
51036
+ server.listen(config.port, "0.0.0.0", function () {
51037
+ process.stdout.write(
51038
+ "[plugin-host] listening type=" + config.type + " port=" + config.port + " version=" + VERSION + "\n"
51039
+ );
51040
+ });
51041
+ `;
51042
+ function pluginHostContentHash() {
51043
+ return (0, import_node_crypto15.createHash)("sha256").update(`${PLUGIN_HOST_VERSION}:${PLUGIN_HOST_SERVER_SOURCE}`).digest("hex").slice(0, 16);
51044
+ }
51045
+
51046
+ // src/sandbox_plugin/host/pluginHostInstaller.ts
51047
+ var PLUGIN_HOST_DIR = "/root/.agents/.plugin-host";
51048
+ var SERVER_PATH = `${PLUGIN_HOST_DIR}/server.js`;
51049
+ var VERSION_PATH = `${PLUGIN_HOST_DIR}/version.json`;
51050
+ async function installPluginHost(sandbox) {
51051
+ const hash = pluginHostContentHash();
51052
+ try {
51053
+ const existing = await sandbox.file.readFile(VERSION_PATH);
51054
+ const parsed = JSON.parse(existing.content);
51055
+ if (parsed.hash === hash) {
51056
+ return { path: SERVER_PATH, version: PLUGIN_HOST_VERSION, hash, changed: false };
51057
+ }
51058
+ } catch {
51059
+ }
51060
+ await sandbox.file.createDirectory(PLUGIN_HOST_DIR).catch(() => void 0);
51061
+ await sandbox.file.writeFile(SERVER_PATH, PLUGIN_HOST_SERVER_SOURCE);
51062
+ await sandbox.file.writeFile(VERSION_PATH, JSON.stringify({ version: PLUGIN_HOST_VERSION, hash }, null, 2));
51063
+ return { path: SERVER_PATH, version: PLUGIN_HOST_VERSION, hash, changed: true };
51064
+ }
51065
+
49298
51066
  // src/workflow/index.ts
49299
51067
  init_compile();
49300
51068
  init_parse_yaml();
@@ -49686,16 +51454,16 @@ var IdRemapper = class {
49686
51454
  };
49687
51455
 
49688
51456
  // src/util/directRoomId.ts
49689
- var import_node_crypto14 = require("crypto");
51457
+ var import_node_crypto16 = require("crypto");
49690
51458
  function generateDirectHumanProjectId(tenantId2, workspaceId, userId1, userId2) {
49691
51459
  const sortedUserIds = [userId1, userId2].sort();
49692
51460
  const parts = [tenantId2, workspaceId, ...sortedUserIds];
49693
- const hash = (0, import_node_crypto14.createHash)("sha256").update(parts.join(":")).digest("hex");
51461
+ const hash = (0, import_node_crypto16.createHash)("sha256").update(parts.join(":")).digest("hex");
49694
51462
  return `direct_human_${hash}`;
49695
51463
  }
49696
51464
  function generateDirectBotProjectId(tenantId2, workspaceId, userId, assistantId) {
49697
51465
  const parts = [tenantId2, workspaceId, userId, assistantId];
49698
- const hash = (0, import_node_crypto14.createHash)("sha256").update(parts.join(":")).digest("hex");
51466
+ const hash = (0, import_node_crypto16.createHash)("sha256").update(parts.join(":")).digest("hex");
49699
51467
  return `direct_bot_${hash}`;
49700
51468
  }
49701
51469
  function parseDirectRoomType(projectId) {
@@ -49725,6 +51493,7 @@ registerBuiltinPlugins();
49725
51493
  CapabilityBundleValidationError,
49726
51494
  CapabilityToolNameConflictError,
49727
51495
  CapabilityToolRegistry,
51496
+ ChannelConnectionLeader,
49728
51497
  ChunkBuffer,
49729
51498
  ChunkBufferLatticeManager,
49730
51499
  CollectionLatticeManager,
@@ -49736,7 +51505,9 @@ registerBuiltinPlugins();
49736
51505
  DEFAULT_CALIBRATION_PROBES,
49737
51506
  DEFAULT_CONNECT_TIMEOUT_MS,
49738
51507
  DEFAULT_DELAY_MS,
51508
+ DEFAULT_HANDLER_TIMEOUT_MS,
49739
51509
  DEFAULT_MAX_ATTEMPTS,
51510
+ DEFAULT_RESIDENT_INVOKE_TIMEOUT_MS,
49740
51511
  DaytonaInstance,
49741
51512
  DaytonaProvider,
49742
51513
  DefaultScheduleClient,
@@ -49754,6 +51525,7 @@ registerBuiltinPlugins();
49754
51525
  HEALTH_CHECK_INTERVAL_MS,
49755
51526
  HEALTH_CHECK_TIMEOUT_MS,
49756
51527
  HumanMessage,
51528
+ INLINE_INPUT_LIMIT_BYTES,
49757
51529
  IdRemapper,
49758
51530
  InMemoryA2AApiKeyStore,
49759
51531
  InMemoryAgentWebAppStore,
@@ -49794,10 +51566,12 @@ registerBuiltinPlugins();
49794
51566
  LocalSandboxProvider,
49795
51567
  LoggerLatticeManager,
49796
51568
  MAX_BACKOFF_MS,
51569
+ MAX_HANDLER_TIMEOUT_MS,
49797
51570
  MAX_LINE_LENGTH,
49798
51571
  MAX_PACK_ENTRIES,
49799
51572
  MAX_PACK_FILE_BYTES,
49800
51573
  MAX_PACK_TOTAL_BYTES,
51574
+ MIN_HANDLER_TIMEOUT_MS,
49801
51575
  ManagedTaskStartError,
49802
51576
  McpLatticeManager,
49803
51577
  MemoryBackend,
@@ -49816,8 +51590,12 @@ registerBuiltinPlugins();
49816
51590
  PACK_DIRECTORY_TIMEOUT_SECONDS,
49817
51591
  PLATFORM_SECRET_ENV_VARS,
49818
51592
  PLUGIN_CATALOG_CHANGED_TOPIC,
51593
+ PLUGIN_HOST_DIR,
51594
+ PLUGIN_HOST_SERVER_SOURCE,
51595
+ PLUGIN_HOST_VERSION,
49819
51596
  PersonalAssistantConfig,
49820
51597
  PinoLoggerClient,
51598
+ PluginDeploymentRuntimeRegistry,
49821
51599
  PluginRegistry,
49822
51600
  PostgresDatabase,
49823
51601
  ProjectBotMembershipIdConflictError,
@@ -49879,7 +51657,9 @@ registerBuiltinPlugins();
49879
51657
  appendUiFence,
49880
51658
  assertSafeUpstreamUrl,
49881
51659
  buildBundleFromDist,
51660
+ buildConnectionPayload,
49882
51661
  buildGrepResultsDict,
51662
+ buildHandlerCommand,
49883
51663
  buildInput,
49884
51664
  buildNamedVolumeName,
49885
51665
  buildRemotePackDirectoryCommand,
@@ -49947,9 +51727,11 @@ registerBuiltinPlugins();
49947
51727
  decrypt,
49948
51728
  decryptConfigFields,
49949
51729
  describeCronExpression,
51730
+ disconnectAllChannels,
49950
51731
  documentLearningPlugin,
49951
51732
  documentParserPlugin,
49952
51733
  embeddingsLatticeManager,
51734
+ encodeInput,
49953
51735
  encrypt,
49954
51736
  encryptConfigFields,
49955
51737
  ensureBuiltinAgentsForTenant,
@@ -49993,6 +51775,7 @@ registerBuiltinPlugins();
49993
51775
  getModelLattice,
49994
51776
  getNextCronTime,
49995
51777
  getOrCreateCollectionVectorStore,
51778
+ getPluginDeploymentService,
49996
51779
  getPluginSkillManifestVersion,
49997
51780
  getQueueLattice,
49998
51781
  getReplicaId,
@@ -50018,9 +51801,12 @@ registerBuiltinPlugins();
50018
51801
  globSearchFiles,
50019
51802
  grepMatchesFromFiles,
50020
51803
  grepSearchFiles,
51804
+ handlerResultText,
50021
51805
  hasChunkBuffer,
50022
51806
  hasDelegatedWorkerToken,
50023
51807
  hasRuntimeCapabilityPolicy,
51808
+ installPluginHost,
51809
+ invokeResident,
50024
51810
  invokeWithRetry,
50025
51811
  isBuiltInSkill,
50026
51812
  isDirectRoom,
@@ -50048,6 +51834,8 @@ registerBuiltinPlugins();
50048
51834
  parallelLimit,
50049
51835
  parseCronExpression,
50050
51836
  parseDirectRoomType,
51837
+ parseHandlerEnvelope,
51838
+ parseHostArgs,
50051
51839
  parseJudgeVerdict,
50052
51840
  parsePackSize,
50053
51841
  parseQueuedExecutionMode,
@@ -50056,6 +51844,7 @@ registerBuiltinPlugins();
50056
51844
  parseTrustedRunContext,
50057
51845
  parseYaml,
50058
51846
  performStringReplacement,
51847
+ pluginHostContentHash,
50059
51848
  postProcessTaskMutationResponse,
50060
51849
  prepareTaskDispatch,
50061
51850
  projectMutationMutex,
@@ -50094,6 +51883,7 @@ registerBuiltinPlugins();
50094
51883
  renderTemplate,
50095
51884
  resetReplicaIdForTests,
50096
51885
  resolveEnabledPluginSkill,
51886
+ resolveHandlerTimeoutMs,
50097
51887
  resolveJudgeModelKey,
50098
51888
  resolvePath,
50099
51889
  resolvePluginConnections,
@@ -50113,6 +51903,7 @@ registerBuiltinPlugins();
50113
51903
  setBindingRegistry,
50114
51904
  setEvalRunService,
50115
51905
  setMenuRegistry,
51906
+ setPluginDeploymentService,
50116
51907
  setSandboxPluginCatalog,
50117
51908
  skillLatticeManager,
50118
51909
  sqlDatabaseManager,