@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.mjs CHANGED
@@ -13293,6 +13293,12 @@ function capResult(result, cap) {
13293
13293
  const serialized = typeof result === "string" ? result : JSON.stringify(result ?? null);
13294
13294
  return Buffer.byteLength(serialized, "utf8") <= cap ? result : truncate(serialized, cap);
13295
13295
  }
13296
+ function handlerResultText(result) {
13297
+ const value = result.result;
13298
+ if (typeof value === "string") return value;
13299
+ if (value === void 0) return (result.stdout ?? "").trim();
13300
+ return JSON.stringify(value);
13301
+ }
13296
13302
  function errorFromStdout(stdout) {
13297
13303
  let text = stdout;
13298
13304
  try {
@@ -14224,7 +14230,7 @@ function compileSandboxPlugin(args) {
14224
14230
  maxResultBytes: t.maxResultBytes
14225
14231
  });
14226
14232
  if (!result.ok) return result.error ?? "sandbox plugin execution failed";
14227
- const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result ?? null);
14233
+ const text = handlerResultText(result);
14228
14234
  const ref = t.ui ? { kind: "plugin", pluginType: manifest.type, resource: t.ui.resource, displayMode: t.ui.displayMode ?? "inline" } : null;
14229
14235
  return ref ? appendUiFence(text, ref) : text;
14230
14236
  },
@@ -14620,7 +14626,7 @@ var TenantPluginCatalog = class {
14620
14626
  // src/sandbox_plugin/sandboxPluginRuntime.ts
14621
14627
  var catalog = null;
14622
14628
  function buildResidentRouting() {
14623
- const baseUrl = process.env.MICROSANDBOX_SERVICE_URL;
14629
+ const baseUrl = process.env.MICROSANDBOX_SERVICE_BASE_URL;
14624
14630
  if (!baseUrl) return void 0;
14625
14631
  return {
14626
14632
  getDeployment: async (tenantId2, pluginType) => await getPluginDeploymentService()?.getDeployment(tenantId2, pluginType) ?? void 0,
@@ -33447,7 +33453,9 @@ var MicrosandboxServiceClient = class {
33447
33453
  }
33448
33454
  } catch {
33449
33455
  }
33450
- throw new Error(`Microsandbox service request failed: ${detail}`);
33456
+ const error = new Error(`Microsandbox service request failed: ${detail}`);
33457
+ error.statusCode = response.status;
33458
+ throw error;
33451
33459
  }
33452
33460
  const payload = await response.json();
33453
33461
  if (payload?.success === false) {
@@ -36896,6 +36904,7 @@ var OutboxPoller = class {
36896
36904
  };
36897
36905
 
36898
36906
  // src/services/ProjectMutationMutex.ts
36907
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
36899
36908
  import { snapshotExactRecord as snapshotExactRecord4 } from "@axiom-lattice/protocols";
36900
36909
 
36901
36910
  // src/services/coordinationLease.ts
@@ -36965,16 +36974,24 @@ var ProjectMutationMutex = class {
36965
36974
  constructor() {
36966
36975
  this.queues = /* @__PURE__ */ new Map();
36967
36976
  this.active = /* @__PURE__ */ new Set();
36977
+ /** Project keys already held by the current async context (reentrancy). */
36978
+ this.held = new AsyncLocalStorage2();
36968
36979
  }
36969
36980
  /** Runs one mutation after all earlier mutations for the same Tenant and Project. */
36970
36981
  async runExclusive(scope, operation, signal) {
36971
36982
  const key5 = projectKey(scope);
36983
+ const inherited = this.held.getStore();
36984
+ if (inherited?.has(key5)) return operation();
36972
36985
  const leaseKey = `project-mutation:${encodeURIComponent(scope.tenantId)}:${encodeURIComponent(scope.projectId)}`;
36973
36986
  await this.acquire(key5, signal);
36987
+ const held = new Set(inherited);
36988
+ held.add(key5);
36974
36989
  try {
36975
- const store = getCoordinationLeaseStore();
36976
- if (!store) return await operation();
36977
- return await withLease(store, leaseKey, getReplicaId(), operation, MUTATION_LEASE_OPTIONS);
36990
+ return await this.held.run(held, async () => {
36991
+ const store = getCoordinationLeaseStore();
36992
+ if (!store) return await operation();
36993
+ return await withLease(store, leaseKey, getReplicaId(), operation, MUTATION_LEASE_OPTIONS);
36994
+ });
36978
36995
  } finally {
36979
36996
  this.release(key5);
36980
36997
  }
@@ -37048,6 +37065,7 @@ var INPUT_KEYS = [
37048
37065
  ];
37049
37066
  var REPLY_INPUT_KEYS = ["tenantId", "membershipId", "text", "sourceRoomMessageId", "inputMessageId", "sourceId", "idempotencyKey"];
37050
37067
  var INTERRUPT_INPUT_KEYS = ["tenantId", "membershipId", "text", "sourceRoomMessageId", "inputMessageId"];
37068
+ var CORRELATED_INTERRUPT_INPUT_KEYS = ["tenantId", "membershipId", "correlationKey", "text"];
37051
37069
  var RoomAgentMessageService = class {
37052
37070
  /**
37053
37071
  * Creates a room writer over the durable membership and message stores.
@@ -37095,6 +37113,50 @@ var RoomAgentMessageService = class {
37095
37113
  idempotencyKey: `room-interrupt:${snapshot.sourceRoomMessageId}:${snapshot.membershipId}:${sourceId}`
37096
37114
  });
37097
37115
  }
37116
+ /**
37117
+ * Persists a correlated HITL interrupt that has no human source message.
37118
+ *
37119
+ * @param input Explicit tenant, active membership, caller-supplied correlation key, and text.
37120
+ * @returns A deep clone of the canonical idempotent room message.
37121
+ * @remarks Business-agnostic: the caller owns the correlation key. Scope is
37122
+ * validated against the durable membership; no source message is required.
37123
+ */
37124
+ postCorrelatedInterrupt(input) {
37125
+ const snapshot = exactStringRecord(input, CORRELATED_INTERRUPT_INPUT_KEYS, "Invalid project room interrupt input");
37126
+ return this.persistScoped({
37127
+ tenantId: snapshot.tenantId,
37128
+ membershipId: snapshot.membershipId,
37129
+ text: snapshot.text,
37130
+ sourceId: `interrupt:${snapshot.correlationKey}`,
37131
+ idempotencyKey: `room-interrupt:${snapshot.membershipId}:${snapshot.correlationKey}`
37132
+ });
37133
+ }
37134
+ async persistScoped(input) {
37135
+ const text = input.text.trim();
37136
+ if (text.length < 1 || text.length > 2e4) throw new Error("Invalid project room message text");
37137
+ const member = snapshotMembership(await this.deps.memberships.findById(input.tenantId, input.membershipId));
37138
+ if (!member || member.id !== input.membershipId || member.status !== "active" || member.tenantId !== input.tenantId) {
37139
+ throw new Error("Invalid project room membership");
37140
+ }
37141
+ const candidate = {
37142
+ id: (this.deps.idFactory ?? randomUUID10)(),
37143
+ tenantId: member.tenantId,
37144
+ workspaceId: member.workspaceId,
37145
+ projectId: member.projectId,
37146
+ roomId: member.roomId,
37147
+ author: { type: "bot", membershipId: member.id, assistantId: member.assistantId },
37148
+ content: { type: "text", text },
37149
+ mentions: [],
37150
+ source: "agent",
37151
+ sourceId: input.sourceId,
37152
+ idempotencyKey: input.idempotencyKey
37153
+ };
37154
+ const canonical = snapshotCanonicalScoped(await this.deps.messages.createIdempotent(candidate), candidate);
37155
+ if (!canonical) throw new Error("Invalid canonical project room message");
37156
+ const result = structuredClone(canonical);
37157
+ this.notifyCommitted(result);
37158
+ return result;
37159
+ }
37098
37160
  async persistDerived(input) {
37099
37161
  const source = snapshotSource(await this.deps.messages.findById(input.tenantId, input.sourceRoomMessageId));
37100
37162
  if (!source || source.id !== input.sourceRoomMessageId || source.tenantId !== input.tenantId) throw new Error("Invalid project room message");
@@ -37250,6 +37312,27 @@ function snapshotCanonical(value, expected) {
37250
37312
  createdAt
37251
37313
  };
37252
37314
  }
37315
+ function snapshotCanonicalScoped(value, expected) {
37316
+ const values = exactValues(value, ["id", "tenantId", "workspaceId", "projectId", "roomId", "author", "content", "mentions", "source", "sourceId", "idempotencyKey", "createdAt"], ["replyToMessageId"]);
37317
+ const author = exactValues(values?.author, ["type", "membershipId", "assistantId"]);
37318
+ const content = exactValues(values?.content, ["type", "text"]);
37319
+ const createdAt = date(values?.createdAt);
37320
+ if (!values || !createdAt || typeof values.id !== "string" || values.id.length === 0 || 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;
37321
+ return {
37322
+ id: values.id,
37323
+ tenantId: expected.tenantId,
37324
+ workspaceId: expected.workspaceId,
37325
+ projectId: expected.projectId,
37326
+ roomId: expected.roomId,
37327
+ author: { type: "bot", membershipId: expected.author.membershipId, assistantId: expected.author.assistantId },
37328
+ content: { type: "text", text: expected.content.text },
37329
+ mentions: [],
37330
+ source: "agent",
37331
+ sourceId: expected.sourceId,
37332
+ idempotencyKey: expected.idempotencyKey,
37333
+ createdAt
37334
+ };
37335
+ }
37253
37336
 
37254
37337
  // src/services/ProjectTaskOwnerPolicy.ts
37255
37338
  import { snapshotExactRecord as snapshotExactRecord6 } from "@axiom-lattice/protocols";
@@ -47129,6 +47212,18 @@ On top of that, add any of three combinable classes:
47129
47212
  ## Combinations
47130
47213
  A plugin is the SET of classes it needs, e.g. \`connection + tools + skills\` or \`UI + connection\`.
47131
47214
 
47215
+ ## Scenarios \u2192 skills
47216
+ | Scenario | Read |
47217
+ |---|---|
47218
+ | Tool that returns/transforms data | \`sandbox-plugins-manifest\` + \`sandbox-plugins-handlers\` |
47219
+ | Skills-only (procedural knowledge, no tools) | \`sandbox-plugins-authoring\` (skills) |
47220
+ | Tool needs credentials / resource discovery | \`sandbox-plugins-connection\` |
47221
+ | Tool shows an interactive panel | \`sandbox-plugins-ui\` |
47222
+ | Tool orchestrates platform tools/agents | \`sandbox-plugins-consume-open\` |
47223
+ | Tool callable by external MCP clients | \`sandbox-plugins-expose\` |
47224
+ | Run as a long-lived resident service | \`sandbox-plugins-deploy\` |
47225
+ | Validate / test / debug | \`sandbox-plugins-testing\` |
47226
+
47132
47227
  ## Agents (not a supported authoring path)
47133
47228
  The manifest schema accepts an \`agents\` block and tenant plugin agents are
47134
47229
  registered, but building agents this way is not supported or designed for tenant
@@ -47339,10 +47434,38 @@ lifecycle handshake:
47339
47434
  - \`ui/notifications/tool-result\` \u2192 a \`CallToolResult\` \`{ content: [{ type: "text", text }] }\` (the tool output text, with any \`mcp_app\` fence stripped).
47340
47435
 
47341
47436
  Register the \`message\` listener **before** \`ui/initialize\` so nothing is missed.
47342
- The result carries **TEXT content only** \u2014 no \`structuredContent\` or \`artifact\`;
47343
- parse the JSON yourself if the tool emitted JSON. To re-query or refresh, call
47344
- \`tools/call\` from the View and render its response (the host does not re-push
47345
- \`tool-result\` for your own calls).
47437
+ Read the payload from \`params.content[0].text\` \u2014 the notification is a
47438
+ \`CallToolResult\`, **not** your handler payload at the top level. The result
47439
+ carries **TEXT content only** \u2014 no \`structuredContent\` or \`artifact\`; parse the
47440
+ JSON yourself if the tool emitted JSON (it is the handler's \`result\`, so the
47441
+ handler must wrap it under \`result\`). To re-query or refresh, call \`tools/call\`
47442
+ from the View and render its response (the host does not re-push \`tool-result\`
47443
+ for your own calls).
47444
+
47445
+ When you call \`tools/call\` from the View, the JSON-RPC response \`result\` is the
47446
+ handler's own \`result\` value **directly** \u2014 it is **not wrapped in a**
47447
+ \`CallToolResult\` \`{ content: [...] }\` (that wrapper is only used for the host's
47448
+ \`tool-result\` push). Parse it as your tool's data.
47449
+
47450
+ ## Push vs re-execute (key for debugging)
47451
+ The host pushes \`tool-result\` from the tool result **recorded when the agent ran** \u2014
47452
+ it does **not** re-execute the handler. The push therefore always reflects that
47453
+ run's output (and the handler's \`result\` at that time). A \`tools/call\` from the
47454
+ View, by contrast, **re-executes** the handler live every time.
47455
+
47456
+ So if the push looks stale/wrong but a fresh \`tools/call\` returns the right data,
47457
+ the recorded run predates your change \u2014 re-run the tool (a new agent call) to get
47458
+ a fresh push. Do not assume the push re-reads the handler.
47459
+
47460
+ ## Self-check / debug
47461
+ - Log the raw frame: \`console.log(JSON.stringify(ev.data))\` in the \`message\`
47462
+ listener, and open the browser console inside the sandboxed frame.
47463
+ - If the pushed text looks like the raw handler envelope (e.g. it starts with
47464
+ \`{"ok":true,...\`), the handler omitted the \`result\` wrapper \u2014 the host falls
47465
+ back to the raw stdout instead of your payload. Fix the handler (see
47466
+ \`sandbox-plugins-handlers\`).
47467
+ - The push only happens after \`ui/initialize\` **and**
47468
+ \`ui/notifications/initialized\`; a listener registered later misses it.
47346
47469
 
47347
47470
  \`templates/ui.html\` implements all of this in vanilla JS and renders both payloads
47348
47471
  into a \`#data\` element.
@@ -47462,6 +47585,76 @@ Templates: see \`sandbox-plugins-manifest\` for the exact \`tools[]\` fields; th
47462
47585
  \`templates/plugin.json\` already includes \`"expose": true\` (the tool is grantable via
47463
47586
  an API key but not callable until a key grants it). Remove it for tools that should
47464
47587
  not be externally grantable at all.
47588
+ `,
47589
+ resources: {}
47590
+ },
47591
+ "sandbox-plugins-consume-open": {
47592
+ version: "1.0.0",
47593
+ content: `---
47594
+ name: sandbox-plugins-consume-open
47595
+ 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.
47596
+ ---
47597
+
47598
+ # Calling the platform from a handler (Open MCP)
47599
+
47600
+ A plugin handler can call **the platform's own tools and agents** through the Open
47601
+ MCP endpoint. Reach for this when a tool should orchestrate existing capabilities
47602
+ (read the knowledge base, create a task, invoke an agent) instead of
47603
+ re-implementing them.
47604
+
47605
+ ## What the handler gets
47606
+ When the gateway injects the credential, each handler process starts with:
47607
+ - \`API_GATEWAY_URL\` \u2014 the gateway base URL.
47608
+ - \`OPEN_API_TOKEN\` \u2014 a short-lived bearer **scoped to this tenant's plugin callers**.
47609
+
47610
+ If the deployment does not inject them (no \`PUBLIC_GATEWAY_URL\`), the variables are
47611
+ **absent** \u2014 detect that and degrade gracefully (report "platform tools
47612
+ unavailable") rather than crashing.
47613
+
47614
+ ## Discover, then call
47615
+ Never hardcode the tool list: the callable set is **reflected per tenant** and
47616
+ changes as plugins are exposed. Call \`listTools()\` at runtime.
47617
+ \`\`\`js
47618
+ const { OpenMcpClient } = require("@axiom-lattice/open-mcp-client");
47619
+ // or require("/root/.agents/sdk/open-mcp-client/index.js") when provisioned there
47620
+
47621
+ async function main() {
47622
+ const input = process.env.PLUGIN_INPUT_FILE
47623
+ ? JSON.parse(require("fs").readFileSync(process.env.PLUGIN_INPUT_FILE, "utf8"))
47624
+ : JSON.parse(process.env.PLUGIN_INPUT || "{}");
47625
+
47626
+ const client = OpenMcpClient.fromEnv(); // API_GATEWAY_URL + OPEN_API_TOKEN
47627
+ const tools = await client.listTools(); // exactly what this handler may call
47628
+ const kb = await client.callTool("kb_search", { collection: "docs", query: input.q });
47629
+
47630
+ process.stdout.write(JSON.stringify({ ok: true, result: kb }));
47631
+ }
47632
+ main().catch((err) => {
47633
+ process.stdout.write(JSON.stringify({ ok: false, error: String(err && err.message ? err.message : err) }));
47634
+ });
47635
+ \`\`\`
47636
+
47637
+ - Tool names are \`{domain}_{action}\` (e.g. \`kb_search\`, \`task_manage_task\`).
47638
+ - Args follow the tool's \`inputSchema\`; \`listTools()\` returns name/description.
47639
+ - Return the result through the normal handler envelope (\`{ ok: true, result }\`)
47640
+ so it becomes this tool's output \u2014 see \`sandbox-plugins-handlers\`.
47641
+
47642
+ ## What the handler may call
47643
+ The token carries the conservative sandbox defaults (\`sandbox_*\`, \`kb_search\`)
47644
+ **plus the tenant's grantable Open catalog**: every exposed tenant/global plugin
47645
+ tool and the tenant's agents. A call outside that set fails (tool-not-found or
47646
+ \`GRANT_DENIED\`); retrying will not help.
47647
+
47648
+ ## Client availability
47649
+ The zero-dependency \`@axiom-lattice/open-mcp-client\` must be present in the
47650
+ sandbox: preinstalled in the image, at \`/root/.agents/sdk/open-mcp-client\`, or
47651
+ installed in this run (\`npm install @axiom-lattice/open-mcp-client\`). The CLI is
47652
+ also available: \`npx @axiom-lattice/open-mcp-client tools list --json\`.
47653
+
47654
+ ## Limits
47655
+ - Nested sandbox orchestration is depth-limited (5); past the cap no token is
47656
+ re-issued, so deep recursion fails fast.
47657
+ - Only call what \`listTools()\` returns, and handle a missing tool gracefully.
47465
47658
  `,
47466
47659
  resources: {}
47467
47660
  },
@@ -47502,10 +47695,23 @@ for the single/first connection, or \`data.connections\` (entries of
47502
47695
  \`{ key, config }\`) when the plugin selects multiple connections.
47503
47696
 
47504
47697
  ## Writing output
47505
- Print a JSON envelope to stdout:
47698
+ Every tool handler must print **one** JSON envelope to stdout, with your payload
47699
+ under \`result\`:
47506
47700
  \`\`\`js
47507
- process.stdout.write(JSON.stringify({ ok: true, result: "done" }));
47701
+ process.stdout.write(JSON.stringify({ ok: true, result: <payload> }));
47508
47702
  \`\`\`
47703
+
47704
+ > **The payload MUST go under \`result\`.** \`result\` may be any JSON value
47705
+ > (object, array, string, number).
47706
+ > \`\`\`js
47707
+ > // \u2705 correct \u2014 data under \`result\`
47708
+ > process.stdout.write(JSON.stringify({ ok: true, result: { tasks, count } }));
47709
+ >
47710
+ > // \u274C wrong \u2014 \`result\` missing; the host surfaces the raw stdout (this
47711
+ > // envelope), so callers and MCP App UIs never see \`tasks\`
47712
+ > process.stdout.write(JSON.stringify({ ok: true, tasks, count }));
47713
+ > \`\`\`
47714
+
47509
47715
  Non-JSON stdout is treated as a plain-text result. To fail, either print
47510
47716
  \`{ ok: false, error }\` or exit non-zero (stderr is not required; stdout is
47511
47717
  captured).
@@ -47691,6 +47897,8 @@ Follow the authoring skill's "Decide first" steps to confirm the goal
47691
47897
  (inputs/outputs). If the UI class applies, load "sandbox-plugins-ui" before
47692
47898
  authoring it. If the tool must be callable by external MCP clients via an API key,
47693
47899
  load "sandbox-plugins-expose" and follow it (\`expose: true\` + key grants).
47900
+ If a tool must call the platform's own tools/agents from its handler, load
47901
+ "sandbox-plugins-consume-open".
47694
47902
 
47695
47903
  Plan with persistent Tasks: MANDATORY until you load "task-tracking" with the
47696
47904
  \`skill\` tool, call neither \`manage_task\` nor any other task operation. Use
@@ -47897,7 +48105,6 @@ var SANDBOX_PLUGINS_UI_REF = {
47897
48105
  resource: SANDBOX_PLUGINS_UI_RESOURCE,
47898
48106
  displayMode: "inline"
47899
48107
  };
47900
- var PLUGIN_BUILDER_SKILLS = [...Object.keys(SANDBOX_PLUGIN_SKILLS), "task-tracking"];
47901
48108
  function tenantIdFrom(exeConfig) {
47902
48109
  const c = exeConfig;
47903
48110
  return c?.configurable?.runConfig?.tenantId ?? "default";
@@ -48013,9 +48220,9 @@ var sandboxPluginsPlugin = {
48013
48220
  id: "skill",
48014
48221
  type: "skill",
48015
48222
  name: "Skill",
48016
- description: "Load the sandbox-plugins authoring, patterns, manifest, handler, connection, UI, and testing skills",
48223
+ description: "Load any available skill, including the sandbox-plugins authoring, patterns, manifest, handler, connection, UI, and testing skills",
48017
48224
  enabled: true,
48018
- config: { readAll: false, skills: PLUGIN_BUILDER_SKILLS }
48225
+ config: { readAll: true }
48019
48226
  },
48020
48227
  {
48021
48228
  id: "task",
@@ -48196,7 +48403,7 @@ var sandboxPluginsPlugin = {
48196
48403
  assistantId: typeof runCfg.assistant_id === "string" ? runCfg.assistant_id : void 0
48197
48404
  }
48198
48405
  });
48199
- return JSON.stringify(redactSecretValues(result, secrets));
48406
+ return handlerResultText(redactSecretValues(result, secrets));
48200
48407
  } catch (err) {
48201
48408
  return JSON.stringify(redactSecretValues({ ok: false, error: errorText(err) }, secrets));
48202
48409
  }
@@ -49573,6 +49780,7 @@ export {
49573
49780
  globSearchFiles,
49574
49781
  grepMatchesFromFiles,
49575
49782
  grepSearchFiles,
49783
+ handlerResultText,
49576
49784
  hasChunkBuffer,
49577
49785
  hasDelegatedWorkerToken,
49578
49786
  hasRuntimeCapabilityPolicy,