@axiom-lattice/core 6.4.0 → 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
@@ -1929,6 +1929,7 @@ __export(index_exports, {
1929
1929
  globSearchFiles: () => globSearchFiles,
1930
1930
  grepMatchesFromFiles: () => grepMatchesFromFiles,
1931
1931
  grepSearchFiles: () => grepSearchFiles,
1932
+ handlerResultText: () => handlerResultText,
1932
1933
  hasChunkBuffer: () => hasChunkBuffer,
1933
1934
  hasDelegatedWorkerToken: () => hasDelegatedWorkerToken,
1934
1935
  hasRuntimeCapabilityPolicy: () => hasRuntimeCapabilityPolicy,
@@ -15322,6 +15323,12 @@ function capResult(result, cap) {
15322
15323
  const serialized = typeof result === "string" ? result : JSON.stringify(result ?? null);
15323
15324
  return Buffer.byteLength(serialized, "utf8") <= cap ? result : truncate(serialized, cap);
15324
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
+ }
15325
15332
  function errorFromStdout(stdout) {
15326
15333
  let text = stdout;
15327
15334
  try {
@@ -16253,7 +16260,7 @@ function compileSandboxPlugin(args) {
16253
16260
  maxResultBytes: t.maxResultBytes
16254
16261
  });
16255
16262
  if (!result.ok) return result.error ?? "sandbox plugin execution failed";
16256
- const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result ?? null);
16263
+ const text = handlerResultText(result);
16257
16264
  const ref = t.ui ? { kind: "plugin", pluginType: manifest.type, resource: t.ui.resource, displayMode: t.ui.displayMode ?? "inline" } : null;
16258
16265
  return ref ? appendUiFence(text, ref) : text;
16259
16266
  },
@@ -16649,7 +16656,7 @@ var TenantPluginCatalog = class {
16649
16656
  // src/sandbox_plugin/sandboxPluginRuntime.ts
16650
16657
  var catalog = null;
16651
16658
  function buildResidentRouting() {
16652
- const baseUrl = process.env.MICROSANDBOX_SERVICE_URL;
16659
+ const baseUrl = process.env.MICROSANDBOX_SERVICE_BASE_URL;
16653
16660
  if (!baseUrl) return void 0;
16654
16661
  return {
16655
16662
  getDeployment: async (tenantId2, pluginType) => await getPluginDeploymentService()?.getDeployment(tenantId2, pluginType) ?? void 0,
@@ -35469,7 +35476,9 @@ var MicrosandboxServiceClient = class {
35469
35476
  }
35470
35477
  } catch {
35471
35478
  }
35472
- 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;
35473
35482
  }
35474
35483
  const payload = await response.json();
35475
35484
  if (payload?.success === false) {
@@ -38918,6 +38927,7 @@ var OutboxPoller = class {
38918
38927
  };
38919
38928
 
38920
38929
  // src/services/ProjectMutationMutex.ts
38930
+ var import_node_async_hooks = require("async_hooks");
38921
38931
  var import_protocols28 = require("@axiom-lattice/protocols");
38922
38932
 
38923
38933
  // src/services/coordinationLease.ts
@@ -38987,16 +38997,24 @@ var ProjectMutationMutex = class {
38987
38997
  constructor() {
38988
38998
  this.queues = /* @__PURE__ */ new Map();
38989
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();
38990
39002
  }
38991
39003
  /** Runs one mutation after all earlier mutations for the same Tenant and Project. */
38992
39004
  async runExclusive(scope, operation, signal) {
38993
39005
  const key5 = projectKey(scope);
39006
+ const inherited = this.held.getStore();
39007
+ if (inherited?.has(key5)) return operation();
38994
39008
  const leaseKey = `project-mutation:${encodeURIComponent(scope.tenantId)}:${encodeURIComponent(scope.projectId)}`;
38995
39009
  await this.acquire(key5, signal);
39010
+ const held = new Set(inherited);
39011
+ held.add(key5);
38996
39012
  try {
38997
- const store = getCoordinationLeaseStore();
38998
- if (!store) return await operation();
38999
- 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
+ });
39000
39018
  } finally {
39001
39019
  this.release(key5);
39002
39020
  }
@@ -39070,6 +39088,7 @@ var INPUT_KEYS = [
39070
39088
  ];
39071
39089
  var REPLY_INPUT_KEYS = ["tenantId", "membershipId", "text", "sourceRoomMessageId", "inputMessageId", "sourceId", "idempotencyKey"];
39072
39090
  var INTERRUPT_INPUT_KEYS = ["tenantId", "membershipId", "text", "sourceRoomMessageId", "inputMessageId"];
39091
+ var CORRELATED_INTERRUPT_INPUT_KEYS = ["tenantId", "membershipId", "correlationKey", "text"];
39073
39092
  var RoomAgentMessageService = class {
39074
39093
  /**
39075
39094
  * Creates a room writer over the durable membership and message stores.
@@ -39117,6 +39136,50 @@ var RoomAgentMessageService = class {
39117
39136
  idempotencyKey: `room-interrupt:${snapshot.sourceRoomMessageId}:${snapshot.membershipId}:${sourceId}`
39118
39137
  });
39119
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
+ }
39120
39183
  async persistDerived(input) {
39121
39184
  const source = snapshotSource(await this.deps.messages.findById(input.tenantId, input.sourceRoomMessageId));
39122
39185
  if (!source || source.id !== input.sourceRoomMessageId || source.tenantId !== input.tenantId) throw new Error("Invalid project room message");
@@ -39272,6 +39335,27 @@ function snapshotCanonical(value, expected) {
39272
39335
  createdAt
39273
39336
  };
39274
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
+ }
39275
39359
 
39276
39360
  // src/services/ProjectTaskOwnerPolicy.ts
39277
39361
  var import_protocols30 = require("@axiom-lattice/protocols");
@@ -49141,6 +49225,18 @@ On top of that, add any of three combinable classes:
49141
49225
  ## Combinations
49142
49226
  A plugin is the SET of classes it needs, e.g. \`connection + tools + skills\` or \`UI + connection\`.
49143
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
+
49144
49240
  ## Agents (not a supported authoring path)
49145
49241
  The manifest schema accepts an \`agents\` block and tenant plugin agents are
49146
49242
  registered, but building agents this way is not supported or designed for tenant
@@ -49351,10 +49447,38 @@ lifecycle handshake:
49351
49447
  - \`ui/notifications/tool-result\` \u2192 a \`CallToolResult\` \`{ content: [{ type: "text", text }] }\` (the tool output text, with any \`mcp_app\` fence stripped).
49352
49448
 
49353
49449
  Register the \`message\` listener **before** \`ui/initialize\` so nothing is missed.
49354
- The result carries **TEXT content only** \u2014 no \`structuredContent\` or \`artifact\`;
49355
- parse the JSON yourself if the tool emitted JSON. To re-query or refresh, call
49356
- \`tools/call\` from the View and render its response (the host does not re-push
49357
- \`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.
49358
49482
 
49359
49483
  \`templates/ui.html\` implements all of this in vanilla JS and renders both payloads
49360
49484
  into a \`#data\` element.
@@ -49474,6 +49598,76 @@ Templates: see \`sandbox-plugins-manifest\` for the exact \`tools[]\` fields; th
49474
49598
  \`templates/plugin.json\` already includes \`"expose": true\` (the tool is grantable via
49475
49599
  an API key but not callable until a key grants it). Remove it for tools that should
49476
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.
49477
49671
  `,
49478
49672
  resources: {}
49479
49673
  },
@@ -49514,10 +49708,23 @@ for the single/first connection, or \`data.connections\` (entries of
49514
49708
  \`{ key, config }\`) when the plugin selects multiple connections.
49515
49709
 
49516
49710
  ## Writing output
49517
- Print a JSON envelope to stdout:
49711
+ Every tool handler must print **one** JSON envelope to stdout, with your payload
49712
+ under \`result\`:
49518
49713
  \`\`\`js
49519
- process.stdout.write(JSON.stringify({ ok: true, result: "done" }));
49714
+ process.stdout.write(JSON.stringify({ ok: true, result: <payload> }));
49520
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
+
49521
49728
  Non-JSON stdout is treated as a plain-text result. To fail, either print
49522
49729
  \`{ ok: false, error }\` or exit non-zero (stderr is not required; stdout is
49523
49730
  captured).
@@ -49703,6 +49910,8 @@ Follow the authoring skill's "Decide first" steps to confirm the goal
49703
49910
  (inputs/outputs). If the UI class applies, load "sandbox-plugins-ui" before
49704
49911
  authoring it. If the tool must be callable by external MCP clients via an API key,
49705
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".
49706
49915
 
49707
49916
  Plan with persistent Tasks: MANDATORY until you load "task-tracking" with the
49708
49917
  \`skill\` tool, call neither \`manage_task\` nor any other task operation. Use
@@ -49909,7 +50118,6 @@ var SANDBOX_PLUGINS_UI_REF = {
49909
50118
  resource: SANDBOX_PLUGINS_UI_RESOURCE,
49910
50119
  displayMode: "inline"
49911
50120
  };
49912
- var PLUGIN_BUILDER_SKILLS = [...Object.keys(SANDBOX_PLUGIN_SKILLS), "task-tracking"];
49913
50121
  function tenantIdFrom(exeConfig) {
49914
50122
  const c = exeConfig;
49915
50123
  return c?.configurable?.runConfig?.tenantId ?? "default";
@@ -50025,9 +50233,9 @@ var sandboxPluginsPlugin = {
50025
50233
  id: "skill",
50026
50234
  type: "skill",
50027
50235
  name: "Skill",
50028
- 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",
50029
50237
  enabled: true,
50030
- config: { readAll: false, skills: PLUGIN_BUILDER_SKILLS }
50238
+ config: { readAll: true }
50031
50239
  },
50032
50240
  {
50033
50241
  id: "task",
@@ -50208,7 +50416,7 @@ var sandboxPluginsPlugin = {
50208
50416
  assistantId: typeof runCfg.assistant_id === "string" ? runCfg.assistant_id : void 0
50209
50417
  }
50210
50418
  });
50211
- return JSON.stringify(redactSecretValues(result, secrets));
50419
+ return handlerResultText(redactSecretValues(result, secrets));
50212
50420
  } catch (err) {
50213
50421
  return JSON.stringify(redactSecretValues({ ok: false, error: errorText(err) }, secrets));
50214
50422
  }
@@ -51593,6 +51801,7 @@ registerBuiltinPlugins();
51593
51801
  globSearchFiles,
51594
51802
  grepMatchesFromFiles,
51595
51803
  grepSearchFiles,
51804
+ handlerResultText,
51596
51805
  hasChunkBuffer,
51597
51806
  hasDelegatedWorkerToken,
51598
51807
  hasRuntimeCapabilityPolicy,