@granular-software/sdk 0.4.1 → 0.4.2

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
@@ -4488,6 +4488,17 @@ var Session = class {
4488
4488
  this.setupEventHandlers();
4489
4489
  this.setupToolInvokeHandler();
4490
4490
  }
4491
+ buildLegacyEffectContext() {
4492
+ return {
4493
+ effectClientId: this.clientId,
4494
+ sandboxId: "",
4495
+ environmentId: "",
4496
+ sessionId: "",
4497
+ user: {
4498
+ subjectId: ""
4499
+ }
4500
+ };
4501
+ }
4491
4502
  // --- Public API ---
4492
4503
  get document() {
4493
4504
  return this.client.doc;
@@ -4526,155 +4537,30 @@ var Session = class {
4526
4537
  });
4527
4538
  return result;
4528
4539
  }
4529
- /**
4530
- * Publish tools to the sandbox and register handlers for reverse-RPC.
4531
- *
4532
- * Tools can be:
4533
- * - **Instance methods**: set `className` (handler receives `(objectId, params)`)
4534
- * - **Static methods**: set `className` + `static: true` (handler receives `(params)`)
4535
- * - **Global tools**: omit `className` (handler receives `(params)`)
4536
- *
4537
- * Both `inputSchema` and `outputSchema` accept JSON Schema objects. The
4538
- * `outputSchema` drives the return type in the auto-generated TypeScript
4539
- * class declarations that sandbox code imports from `./sandbox-tools`.
4540
- *
4541
- * This method:
4542
- * 1. Extracts tool schemas (including `outputSchema`) from the provided tools
4543
- * 2. Publishes them via `client.publishRawToolCatalog` RPC
4544
- * 3. Registers handlers locally for `tool.invoke` RPC calls
4545
- * 4. Returns the `domainRevision` needed for job submission
4546
- *
4547
- * @param tools - Array of tools with handlers
4548
- * @param revision - Optional revision string (default: "1.0.0")
4549
- * @returns PublishToolsResult with domainRevision
4550
- *
4551
- * @example
4552
- * ```typescript
4553
- * await env.publishTools([
4554
- * {
4555
- * name: 'get_bio',
4556
- * description: 'Get biography of an author',
4557
- * className: 'author',
4558
- * inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
4559
- * outputSchema: { type: 'object', properties: { bio: { type: 'string' } }, required: ['bio'] },
4560
- * handler: async (id, params) => ({ bio: `Bio of ${id}` }),
4561
- * },
4562
- * ]);
4563
- * ```
4564
- */
4565
4540
  async publishTools(tools, revision = "1.0.0") {
4566
- const schemas = tools.map((tool) => ({
4567
- name: tool.name,
4568
- description: tool.description,
4569
- inputSchema: tool.inputSchema,
4570
- outputSchema: tool.outputSchema,
4571
- stability: tool.stability || "stable",
4572
- provenance: tool.provenance || { source: "mcp" },
4573
- tags: tool.tags,
4574
- className: tool.className,
4575
- static: tool.static
4576
- }));
4577
- const result = await this.client.call("client.publishRawToolCatalog", {
4578
- clientId: this.clientId,
4579
- revision,
4580
- tools: schemas
4581
- });
4582
- if (!result.accepted || !result.domainRevision) {
4583
- throw new Error(`Failed to publish tools: ${JSON.stringify(result.rejected)}`);
4584
- }
4585
- for (const tool of tools) {
4586
- this.toolHandlers.set(tool.name, tool.handler);
4587
- if (tool.className && !tool.static) {
4588
- this.instanceTools.add(tool.name);
4589
- } else {
4590
- this.instanceTools.delete(tool.name);
4591
- }
4592
- }
4593
- this.currentDomainRevision = result.domainRevision;
4594
- return {
4595
- accepted: result.accepted,
4596
- domainRevision: result.domainRevision,
4597
- rejected: result.rejected
4598
- };
4541
+ throw new Error(
4542
+ "Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.registerEffects(environment.sandboxId, effects)."
4543
+ );
4599
4544
  }
4600
- /**
4601
- * Publish a single effect (tool) to the sandbox.
4602
- *
4603
- * Adds the effect to the local registry and re-publishes the
4604
- * full tool catalog to the server. If an effect with the same
4605
- * name already exists, it is replaced.
4606
- *
4607
- * @param effect - The effect (tool) to publish
4608
- * @returns PublishToolsResult with domainRevision
4609
- *
4610
- * @example
4611
- * ```typescript
4612
- * await env.publishEffect({
4613
- * name: 'get_bio',
4614
- * description: 'Get biography of an author',
4615
- * className: 'author',
4616
- * inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
4617
- * handler: async (id, params) => ({ bio: `Bio of ${id}` }),
4618
- * });
4619
- * ```
4620
- */
4621
4545
  async publishEffect(effect) {
4622
- this.effects.set(effect.name, effect);
4623
- return this._syncEffects();
4546
+ throw new Error(
4547
+ "Environment-scoped effect publication was removed. Use granular.registerEffect(environment.sandboxId, effect)."
4548
+ );
4624
4549
  }
4625
- /**
4626
- * Publish multiple effects (tools) at once.
4627
- *
4628
- * Adds all effects to the local registry and re-publishes the
4629
- * full tool catalog in a single RPC call.
4630
- *
4631
- * @param effects - Array of effects to publish
4632
- * @returns PublishToolsResult with domainRevision
4633
- *
4634
- * @example
4635
- * ```typescript
4636
- * await env.publishEffects([
4637
- * { name: 'get_bio', description: '...', inputSchema: {}, handler: async (id) => ({}) },
4638
- * { name: 'search', description: '...', inputSchema: {}, handler: async (params) => ({}) },
4639
- * ]);
4640
- * ```
4641
- */
4642
4550
  async publishEffects(effects) {
4643
- for (const effect of effects) {
4644
- this.effects.set(effect.name, effect);
4645
- }
4646
- return this._syncEffects();
4551
+ throw new Error(
4552
+ "Environment-scoped effect publication was removed. Use granular.registerEffects(environment.sandboxId, effects)."
4553
+ );
4647
4554
  }
4648
- /**
4649
- * Remove an effect by name and re-publish the remaining catalog.
4650
- *
4651
- * @param name - The name of the effect to remove
4652
- * @returns PublishToolsResult with domainRevision
4653
- */
4654
4555
  async unpublishEffect(name) {
4655
- this.effects.delete(name);
4656
- this.toolHandlers.delete(name);
4657
- this.instanceTools.delete(name);
4658
- return this._syncEffects();
4556
+ throw new Error(
4557
+ "Environment-scoped effect publication was removed. Use granular.unregisterEffect(environment.sandboxId, effectName)."
4558
+ );
4659
4559
  }
4660
- /**
4661
- * Remove all effects and publish an empty catalog.
4662
- *
4663
- * @returns PublishToolsResult with domainRevision
4664
- */
4665
4560
  async unpublishAllEffects() {
4666
- this.effects.clear();
4667
- this.toolHandlers.clear();
4668
- this.instanceTools.clear();
4669
- return this._syncEffects();
4670
- }
4671
- /**
4672
- * Internal: re-publish the full effect catalog to the server.
4673
- * Called after any mutation to the effects Map.
4674
- */
4675
- async _syncEffects() {
4676
- const allEffects = Array.from(this.effects.values());
4677
- return this.publishTools(allEffects);
4561
+ throw new Error(
4562
+ "Environment-scoped effect publication was removed. Use granular.unregisterAllEffects(environment.sandboxId)."
4563
+ );
4678
4564
  }
4679
4565
  /**
4680
4566
  * Submit a job to execute code in the sandbox.
@@ -4688,14 +4574,14 @@ var Session = class {
4688
4574
  * const books = await tolkien.get_books();
4689
4575
  * ```
4690
4576
  *
4691
- * Tool calls (instance methods, static methods, global functions) trigger
4692
- * `tool.invoke` RPC back to this client, where the registered handlers
4577
+ * Effect calls (instance methods, static methods, global functions) trigger
4578
+ * `effect.invoke` RPC back to the sandbox effect host, where the registered handlers
4693
4579
  * execute locally and return the result to the sandbox.
4694
4580
  */
4695
4581
  async submitJob(code, domainRevision) {
4696
4582
  const revision = domainRevision || this.currentDomainRevision || this.client.doc?.domain?.active || void 0;
4697
4583
  if (!revision) {
4698
- throw new Error("No domain revision available. Call publishTools() or ensure schema is activated.");
4584
+ throw new Error("No domain revision available. Register live effects or ensure the build schema is activated.");
4699
4585
  }
4700
4586
  const result = await this.client.call("job.submit", {
4701
4587
  domainRevision: revision,
@@ -4727,10 +4613,10 @@ var Session = class {
4727
4613
  await this.client.call("prompt.answer", { promptId, value: answer });
4728
4614
  }
4729
4615
  /**
4730
- * Get the current list of available tools.
4731
- * Consolidates tools from all connected clients.
4616
+ * Get the current list of available effects.
4617
+ * Consolidates effect declarations and live availability for the session.
4732
4618
  */
4733
- getTools() {
4619
+ getEffects() {
4734
4620
  const doc = this.client.doc;
4735
4621
  const toolMap = /* @__PURE__ */ new Map();
4736
4622
  const domainPkg = doc.domain?.packages?.domain;
@@ -4775,10 +4661,32 @@ var Session = class {
4775
4661
  return Array.from(toolMap.values());
4776
4662
  }
4777
4663
  /**
4778
- * Subscribe to tool changes (added, removed, updated).
4664
+ * Backwards-compatible alias for `getEffects()`.
4665
+ */
4666
+ getTools() {
4667
+ return this.getEffects();
4668
+ }
4669
+ /**
4670
+ * Subscribe to effect changes (added, removed, updated).
4779
4671
  * @param callback - Function called with change events
4780
4672
  * @returns Unsubscribe function
4781
4673
  */
4674
+ onEffectsChanged(callback) {
4675
+ const handler = (data) => callback(data);
4676
+ if (!this.eventListeners.has("effects:changed")) {
4677
+ this.eventListeners.set("effects:changed", []);
4678
+ }
4679
+ this.eventListeners.get("effects:changed").push(handler);
4680
+ return () => {
4681
+ const listeners = this.eventListeners.get("effects:changed");
4682
+ if (listeners) {
4683
+ this.eventListeners.set("effects:changed", listeners.filter((h) => h !== handler));
4684
+ }
4685
+ };
4686
+ }
4687
+ /**
4688
+ * Backwards-compatible alias for `onEffectsChanged()`.
4689
+ */
4782
4690
  onToolsChanged(callback) {
4783
4691
  const handler = (data) => callback(data);
4784
4692
  if (!this.eventListeners.has("tools:changed")) {
@@ -4891,7 +4799,7 @@ import { ${allImports} } from "./sandbox-tools";
4891
4799
  }
4892
4800
  }
4893
4801
  if (globalTools && globalTools.length > 0) {
4894
- docs2 += "## Global Tools\n\n";
4802
+ docs2 += "## Global Effects\n\n";
4895
4803
  for (const tool of globalTools) {
4896
4804
  docs2 += `### ${tool.name}
4897
4805
 
@@ -4909,10 +4817,10 @@ import { ${allImports} } from "./sandbox-tools";
4909
4817
  return docs2;
4910
4818
  }
4911
4819
  if (!tools || tools.length === 0) {
4912
- return "No tools available in this domain.";
4820
+ return "No effects available in this domain.";
4913
4821
  }
4914
- let docs = "# Available Tools\n\n";
4915
- docs += "Import tools from `./sandbox-tools` and call them with await:\n\n";
4822
+ let docs = "# Available Effects\n\n";
4823
+ docs += "Import effects from `./sandbox-tools` and call them with await:\n\n";
4916
4824
  docs += '```typescript\nimport { tools } from "./sandbox-tools";\n\n';
4917
4825
  docs += "// Example:\n";
4918
4826
  docs += `const result = await tools.${tools[0]?.name || "example"}(input);
@@ -4976,6 +4884,7 @@ import { ${allImports} } from "./sandbox-tools";
4976
4884
  setupToolInvokeHandler() {
4977
4885
  this.client.registerRpcHandler("tool.invoke", async (params) => {
4978
4886
  const { callId, toolName, input } = params;
4887
+ this.emit("effect:invoke", { callId, effectKey: toolName, toolName, input });
4979
4888
  this.emit("tool:invoke", { callId, toolName, input });
4980
4889
  const handler = this.toolHandlers.get(toolName);
4981
4890
  if (!handler) {
@@ -4987,12 +4896,14 @@ import { ${allImports} } from "./sandbox-tools";
4987
4896
  }
4988
4897
  try {
4989
4898
  let result;
4899
+ const invocationContext = this.buildLegacyEffectContext();
4990
4900
  if (this.instanceTools.has(toolName) && input && typeof input === "object" && "_objectId" in input) {
4991
4901
  const { _objectId, ...restParams } = input;
4992
- result = await handler(_objectId, restParams);
4902
+ result = await handler(_objectId, restParams, invocationContext);
4993
4903
  } else {
4994
- result = await handler(input);
4904
+ result = await handler(input, invocationContext);
4995
4905
  }
4906
+ this.emit("effect:result", { callId, effectKey: toolName, result });
4996
4907
  this.emit("tool:result", { callId, result });
4997
4908
  await this.client.call("tool.result", {
4998
4909
  callId,
@@ -5000,6 +4911,7 @@ import { ${allImports} } from "./sandbox-tools";
5000
4911
  });
5001
4912
  } catch (error) {
5002
4913
  const errorMessage = error instanceof Error ? error.message : String(error);
4914
+ this.emit("effect:result", { callId, effectKey: toolName, error: errorMessage });
5003
4915
  this.emit("tool:result", { callId, error: errorMessage });
5004
4916
  await this.client.call("tool.result", {
5005
4917
  callId,
@@ -5032,10 +4944,10 @@ import { ${allImports} } from "./sandbox-tools";
5032
4944
  }
5033
4945
  }
5034
4946
  /**
5035
- * Check for changes in the tool catalog and emit 'tools:changed' if needed
4947
+ * Check for changes in the effect catalog and emit change events if needed.
5036
4948
  */
5037
4949
  checkForToolChanges() {
5038
- const currentTools = this.getTools();
4950
+ const currentTools = this.getEffects();
5039
4951
  const currentMap = new Map(currentTools.map((t) => [t.name, t]));
5040
4952
  const added = [];
5041
4953
  const removed = [];
@@ -5051,6 +4963,12 @@ import { ${allImports} } from "./sandbox-tools";
5051
4963
  }
5052
4964
  if (added.length > 0 || removed.length > 0) {
5053
4965
  this.lastKnownTools = currentMap;
4966
+ this.emit("effects:changed", {
4967
+ effects: currentTools,
4968
+ tools: currentTools,
4969
+ added,
4970
+ removed
4971
+ });
5054
4972
  this.emit("tools:changed", {
5055
4973
  tools: currentTools,
5056
4974
  added,
@@ -5219,6 +5137,37 @@ var STANDARD_MODULES_OPERATIONS = [
5219
5137
  var BUILTIN_MODULES = {
5220
5138
  "standard_modules": STANDARD_MODULES_OPERATIONS
5221
5139
  };
5140
+ function computeEffectKey(effect) {
5141
+ const attachedClass = effect.className?.trim();
5142
+ if (!attachedClass) {
5143
+ return `global:${effect.name}`;
5144
+ }
5145
+ return effect.static ? `class:${attachedClass}:static:${effect.name}` : `class:${attachedClass}:instance:${effect.name}`;
5146
+ }
5147
+ function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId) {
5148
+ const url = new URL(apiUrl);
5149
+ if (url.pathname.endsWith("/granular/ws/connect")) {
5150
+ url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
5151
+ } else if (url.pathname.endsWith("/granular")) {
5152
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/effects/connect`;
5153
+ } else if (url.pathname.endsWith("/v2/ws/connect")) {
5154
+ url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
5155
+ } else if (url.pathname.endsWith("/v2/ws")) {
5156
+ url.pathname = url.pathname.replace(/\/ws$/, "/effects/connect");
5157
+ } else if (url.pathname.endsWith("/ws/connect")) {
5158
+ url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
5159
+ } else if (url.pathname.endsWith("/ws")) {
5160
+ url.pathname = url.pathname.replace(/\/ws$/, "/effects/connect");
5161
+ } else {
5162
+ url.pathname = "/granular/effects/connect";
5163
+ }
5164
+ url.search = "";
5165
+ url.hash = "";
5166
+ url.searchParams.set("sandboxId", sandboxId);
5167
+ url.searchParams.set("effectClientId", effectClientId);
5168
+ url.searchParams.set("clientId", clientId);
5169
+ return url.toString();
5170
+ }
5222
5171
  var Environment = class _Environment extends Session {
5223
5172
  envData;
5224
5173
  _apiKey;
@@ -5963,72 +5912,31 @@ var Environment = class _Environment extends Session {
5963
5912
  }
5964
5913
  // ==================== PUBLISH TOOLS ====================
5965
5914
  /**
5966
- * Convenience method: publish tools and get back wrapped result.
5967
- * This is the main entry point for setting up tools.
5968
- *
5969
- * @example
5970
- * ```typescript
5971
- * const tools = [
5972
- * {
5973
- * name: 'get_weather',
5974
- * description: 'Get weather for a city',
5975
- * inputSchema: { type: 'object', properties: { city: { type: 'string' } } },
5976
- * handler: async ({ city }) => ({ temp: 22 }),
5977
- * },
5978
- * ];
5979
- *
5980
- * const { domainRevision } = await environment.publishTools(tools);
5981
- *
5982
- * // Now submit jobs that use those tools
5983
- * const job = await environment.submitJob(`
5984
- * import { Author } from './sandbox-tools';
5985
- * const weather = await Author.get_weather({ city: 'Paris' });
5986
- * return weather;
5987
- * `);
5988
- *
5989
- * const result = await job.result;
5990
- * ```
5915
+ * Removed: environment-scoped effect publication is no longer supported.
5991
5916
  */
5992
5917
  async publishTools(tools, revision = "1.0.0") {
5993
5918
  return super.publishTools(tools, revision);
5994
5919
  }
5995
5920
  /**
5996
- * Publish a single effect (tool) incrementally.
5997
- *
5998
- * Adds the effect to the local registry and re-publishes the
5999
- * full tool catalog to the server. If an effect with the same
6000
- * name already exists, it is replaced.
6001
- *
6002
- * @example
6003
- * ```typescript
6004
- * await env.publishEffect({
6005
- * name: 'get_bio',
6006
- * description: 'Get biography',
6007
- * inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
6008
- * handler: async (params) => ({ bio: 'Hello' }),
6009
- * });
6010
- * ```
5921
+ * Removed: environment-scoped effect publication is no longer supported.
6011
5922
  */
6012
5923
  async publishEffect(effect) {
6013
5924
  return super.publishEffect(effect);
6014
5925
  }
6015
5926
  /**
6016
- * Publish multiple effects (tools) at once.
6017
- *
6018
- * Adds all effects to the local registry and re-publishes the
6019
- * full tool catalog in a single RPC call.
5927
+ * Removed: environment-scoped effect publication is no longer supported.
6020
5928
  */
6021
5929
  async publishEffects(effects) {
6022
5930
  return super.publishEffects(effects);
6023
5931
  }
6024
5932
  /**
6025
- * Remove an effect by name and re-publish the remaining catalog.
5933
+ * Removed: environment-scoped effect publication is no longer supported.
6026
5934
  */
6027
5935
  async unpublishEffect(name) {
6028
5936
  return super.unpublishEffect(name);
6029
5937
  }
6030
5938
  /**
6031
- * Remove all effects and publish an empty catalog.
5939
+ * Removed: environment-scoped effect publication is no longer supported.
6032
5940
  */
6033
5941
  async unpublishAllEffects() {
6034
5942
  return super.unpublishAllEffects();
@@ -6042,10 +5950,12 @@ var Granular = class {
6042
5950
  WebSocketCtor;
6043
5951
  onUnexpectedClose;
6044
5952
  onReconnectError;
6045
- /** Sandbox-level effect registry: sandboxId → (toolName → ToolWithHandler) */
5953
+ /** Sandbox-level effect registry: sandboxId → (effectKey → ToolWithHandler) */
6046
5954
  sandboxEffects = /* @__PURE__ */ new Map();
6047
- /** Active environments tracker: sandboxId Environment[] */
6048
- activeEnvironments = /* @__PURE__ */ new Map();
5955
+ /** Live sandbox-scoped effect hosts keyed by sandboxId */
5956
+ sandboxEffectHosts = /* @__PURE__ */ new Map();
5957
+ /** In-flight host connection promises to avoid duplicate concurrent connects */
5958
+ sandboxEffectHostPromises = /* @__PURE__ */ new Map();
6049
5959
  /**
6050
5960
  * Create a new Granular client
6051
5961
  * @param options - Client configuration
@@ -6098,8 +6008,9 @@ var Granular = class {
6098
6008
  /**
6099
6009
  * Connect to a sandbox and establish a real-time environment session.
6100
6010
  *
6101
- * After connecting, use `environment.publishTools()` to register tools,
6102
- * then `environment.submitJob()` to execute code that uses those tools.
6011
+ * Effects are registered at the sandbox level via `granular.registerEffect()`
6012
+ * or `granular.registerEffects()`. Sessions pick up live availability from
6013
+ * the sandbox registry automatically.
6103
6014
  *
6104
6015
  * @param options - Connection options
6105
6016
  * @returns An active environment session
@@ -6116,10 +6027,12 @@ var Granular = class {
6116
6027
  * user,
6117
6028
  * });
6118
6029
  *
6119
- * // Publish tools
6120
- * await environment.publishTools([
6121
- * { name: 'greet', description: 'Say hello', inputSchema: {}, handler: async () => 'Hello!' },
6122
- * ]);
6030
+ * await granular.registerEffect('my-sandbox', {
6031
+ * name: 'greet',
6032
+ * description: 'Say hello',
6033
+ * inputSchema: { type: 'object', properties: {} },
6034
+ * handler: async () => 'Hello!',
6035
+ * });
6123
6036
  *
6124
6037
  * // Submit job
6125
6038
  * const job = await environment.submitJob(`
@@ -6141,10 +6054,18 @@ var Granular = class {
6141
6054
  subjectId: options.user.subjectId,
6142
6055
  permissionProfileId: null
6143
6056
  });
6057
+ await this.activateEnvironment(envData.environmentId);
6058
+ const session = await this.request("/ws/sessions", {
6059
+ method: "POST",
6060
+ body: JSON.stringify({
6061
+ environmentId: envData.environmentId,
6062
+ clientId
6063
+ })
6064
+ });
6144
6065
  const client = new WSClient({
6145
- url: this.apiUrl,
6146
- sessionId: envData.environmentId,
6147
- token: this.apiKey,
6066
+ url: session.wsUrl,
6067
+ sessionId: session.sessionId,
6068
+ token: session.token,
6148
6069
  tokenProvider: this.tokenProvider,
6149
6070
  WebSocketCtor: this.WebSocketCtor,
6150
6071
  onUnexpectedClose: this.onUnexpectedClose,
@@ -6153,47 +6074,165 @@ var Granular = class {
6153
6074
  await client.connect();
6154
6075
  const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
6155
6076
  const environment = new Environment(client, envData, clientId, this.apiKey, graphqlEndpoint);
6156
- if (!this.activeEnvironments.has(sandbox.sandboxId)) {
6157
- this.activeEnvironments.set(sandbox.sandboxId, []);
6158
- }
6159
- this.activeEnvironments.get(sandbox.sandboxId).push(environment);
6160
- environment.on("disconnect", () => {
6161
- const list = this.activeEnvironments.get(sandbox.sandboxId);
6162
- if (list) {
6163
- this.activeEnvironments.set(sandbox.sandboxId, list.filter((e) => e !== environment));
6164
- }
6165
- });
6166
6077
  await environment.hello();
6167
- const effects = this.sandboxEffects.get(sandbox.sandboxId);
6168
- if (effects && effects.size > 0) {
6169
- const effectsList = Array.from(effects.values());
6170
- console.log(`[Granular] Auto-publishing ${effectsList.length} effects for sandbox ${sandbox.sandboxId}`);
6171
- await environment.publishEffects(effectsList);
6172
- }
6173
6078
  return environment;
6174
6079
  }
6080
+ async activateEnvironment(environmentId) {
6081
+ await this.request(`/orchestrator/runtime/environments/${environmentId}/activate`, {
6082
+ method: "POST",
6083
+ body: JSON.stringify({})
6084
+ });
6085
+ }
6175
6086
  // ── Sandbox-Level Effects ──
6087
+ getSandboxEffectMap(sandboxId) {
6088
+ let effects = this.sandboxEffects.get(sandboxId);
6089
+ if (!effects) {
6090
+ effects = /* @__PURE__ */ new Map();
6091
+ this.sandboxEffects.set(sandboxId, effects);
6092
+ }
6093
+ return effects;
6094
+ }
6095
+ serializeEffect(effect) {
6096
+ return {
6097
+ effectKey: computeEffectKey(effect),
6098
+ name: effect.name,
6099
+ description: effect.description,
6100
+ inputSchema: effect.inputSchema,
6101
+ outputSchema: effect.outputSchema,
6102
+ stability: effect.stability || "stable",
6103
+ provenance: effect.provenance || { source: "custom" },
6104
+ tags: effect.tags,
6105
+ className: effect.className,
6106
+ static: effect.static
6107
+ };
6108
+ }
6109
+ async publishSandboxEffectCatalog(host) {
6110
+ const effects = Array.from(this.getSandboxEffectMap(host.sandboxId).values()).map(
6111
+ (effect) => this.serializeEffect(effect)
6112
+ );
6113
+ await host.wsClient.call("effects.publishCatalog", { effects });
6114
+ }
6115
+ async syncSandboxEffectCatalog(sandboxId) {
6116
+ const host = await this.ensureSandboxEffectHost(sandboxId);
6117
+ await this.publishSandboxEffectCatalog(host);
6118
+ }
6119
+ startEffectHostHeartbeat(host) {
6120
+ if (host.heartbeatTimer) {
6121
+ clearInterval(host.heartbeatTimer);
6122
+ }
6123
+ host.wsClient.call("client.heartbeat", {}).catch((error) => {
6124
+ console.warn(
6125
+ `[Granular] Initial effect host heartbeat failed for sandbox ${host.sandboxId}:`,
6126
+ error
6127
+ );
6128
+ });
6129
+ host.heartbeatTimer = setInterval(() => {
6130
+ host.wsClient.call("client.heartbeat", {}).catch((error) => {
6131
+ console.warn(
6132
+ `[Granular] Effect host heartbeat failed for sandbox ${host.sandboxId}:`,
6133
+ error
6134
+ );
6135
+ });
6136
+ }, 1e4);
6137
+ }
6138
+ stopEffectHostHeartbeat(host) {
6139
+ if (!host?.heartbeatTimer) {
6140
+ return;
6141
+ }
6142
+ clearInterval(host.heartbeatTimer);
6143
+ host.heartbeatTimer = null;
6144
+ }
6145
+ async synchronizeEffectHost(host) {
6146
+ await host.wsClient.call("client.hello", {
6147
+ clientId: host.clientId,
6148
+ protocolVersion: "2.0"
6149
+ });
6150
+ this.startEffectHostHeartbeat(host);
6151
+ await this.publishSandboxEffectCatalog(host);
6152
+ }
6153
+ async ensureSandboxEffectHost(sandboxId) {
6154
+ const existing = this.sandboxEffectHosts.get(sandboxId);
6155
+ if (existing) {
6156
+ return existing;
6157
+ }
6158
+ const inflight = this.sandboxEffectHostPromises.get(sandboxId);
6159
+ if (inflight) {
6160
+ return inflight;
6161
+ }
6162
+ const connectPromise = (async () => {
6163
+ const effectClientId = crypto.randomUUID();
6164
+ const clientId = `effect-host:${sandboxId}:${effectClientId}`;
6165
+ const wsClient = new WSClient({
6166
+ url: buildEffectHostUrl(this.apiUrl, sandboxId, effectClientId, clientId),
6167
+ sessionId: `effect-host:${effectClientId}`,
6168
+ token: this.apiKey,
6169
+ tokenProvider: this.tokenProvider,
6170
+ WebSocketCtor: this.WebSocketCtor,
6171
+ onUnexpectedClose: this.onUnexpectedClose,
6172
+ onReconnectError: this.onReconnectError
6173
+ });
6174
+ const host = {
6175
+ sandboxId,
6176
+ effectClientId,
6177
+ clientId,
6178
+ wsClient,
6179
+ heartbeatTimer: null
6180
+ };
6181
+ wsClient.registerRpcHandler("effect.invoke", async (params) => {
6182
+ const request = params;
6183
+ const effect = this.getSandboxEffectMap(sandboxId).get(request.effectKey);
6184
+ if (!effect) {
6185
+ throw new Error(`Effect handler not found: ${request.effectKey}`);
6186
+ }
6187
+ if (effect.className && !effect.static && request.input && typeof request.input === "object" && "_objectId" in request.input) {
6188
+ const { _objectId, ...rest } = request.input;
6189
+ return effect.handler(_objectId, rest, request.context);
6190
+ }
6191
+ return effect.handler(request.input, request.context);
6192
+ });
6193
+ wsClient.on("open", () => {
6194
+ void this.synchronizeEffectHost(host).catch((error) => {
6195
+ console.error(
6196
+ `[Granular] Failed to re-sync effect host for sandbox ${sandboxId}:`,
6197
+ error
6198
+ );
6199
+ });
6200
+ });
6201
+ wsClient.on("disconnect", () => {
6202
+ this.stopEffectHostHeartbeat(host);
6203
+ });
6204
+ await wsClient.connect();
6205
+ await this.synchronizeEffectHost(host);
6206
+ this.sandboxEffectHosts.set(sandboxId, host);
6207
+ return host;
6208
+ })();
6209
+ this.sandboxEffectHostPromises.set(sandboxId, connectPromise);
6210
+ try {
6211
+ return await connectPromise;
6212
+ } finally {
6213
+ this.sandboxEffectHostPromises.delete(sandboxId);
6214
+ }
6215
+ }
6216
+ disconnectSandboxEffectHost(sandboxId) {
6217
+ const host = this.sandboxEffectHosts.get(sandboxId);
6218
+ if (!host) {
6219
+ return;
6220
+ }
6221
+ this.stopEffectHostHeartbeat(host);
6222
+ host.wsClient.disconnect();
6223
+ this.sandboxEffectHosts.delete(sandboxId);
6224
+ }
6176
6225
  /**
6177
6226
  * Register an effect (tool) for a specific sandbox.
6178
- *
6179
- * The effect will be automatically published to:
6180
- * 1. Any currently active environments for this sandbox
6181
- * 2. Any new environments created/connected for this sandbox
6182
- *
6227
+ *
6183
6228
  * @param sandboxNameOrId - The name or ID of the sandbox
6184
6229
  * @param effect - The tool definition and handler
6185
6230
  */
6186
6231
  async registerEffect(sandboxNameOrId, effect) {
6187
6232
  const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
6188
6233
  const sandboxId = sandbox.sandboxId;
6189
- if (!this.sandboxEffects.has(sandboxId)) {
6190
- this.sandboxEffects.set(sandboxId, /* @__PURE__ */ new Map());
6191
- }
6192
- this.sandboxEffects.get(sandboxId).set(effect.name, effect);
6193
- const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
6194
- for (const env of activeEnvs) {
6195
- await env.publishEffect(effect);
6196
- }
6234
+ this.getSandboxEffectMap(sandboxId).set(computeEffectKey(effect), effect);
6235
+ await this.syncSandboxEffectCatalog(sandboxId);
6197
6236
  }
6198
6237
  /**
6199
6238
  * Register multiple effects (tools) for a specific sandbox.
@@ -6203,35 +6242,39 @@ var Granular = class {
6203
6242
  async registerEffects(sandboxNameOrId, effects) {
6204
6243
  const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
6205
6244
  const sandboxId = sandbox.sandboxId;
6206
- if (!this.sandboxEffects.has(sandboxId)) {
6207
- this.sandboxEffects.set(sandboxId, /* @__PURE__ */ new Map());
6208
- }
6209
- const map = this.sandboxEffects.get(sandboxId);
6245
+ const map = this.getSandboxEffectMap(sandboxId);
6210
6246
  for (const effect of effects) {
6211
- map.set(effect.name, effect);
6212
- }
6213
- const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
6214
- for (const env of activeEnvs) {
6215
- await env.publishEffects(effects);
6247
+ map.set(computeEffectKey(effect), effect);
6216
6248
  }
6249
+ await this.syncSandboxEffectCatalog(sandboxId);
6217
6250
  }
6218
6251
  /**
6219
6252
  * Unregister an effect from a sandbox.
6220
6253
  *
6221
- * Removes it from the local registry and unpublishes it from
6222
- * all active environments.
6254
+ * Removes it from the local sandbox registry and updates the
6255
+ * sandbox-scoped live catalog.
6223
6256
  */
6224
6257
  async unregisterEffect(sandboxNameOrId, name) {
6225
6258
  const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
6226
6259
  const sandboxId = sandbox.sandboxId;
6227
- const map = this.sandboxEffects.get(sandboxId);
6228
- if (map) {
6229
- map.delete(name);
6260
+ const currentMap = this.sandboxEffects.get(sandboxId);
6261
+ if (!currentMap) {
6262
+ return;
6230
6263
  }
6231
- const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
6232
- for (const env of activeEnvs) {
6233
- await env.unpublishEffect(name);
6264
+ const nextEntries = Array.from(currentMap.entries()).filter(
6265
+ ([effectKey, effect]) => effectKey !== name && effect.name !== name
6266
+ );
6267
+ if (nextEntries.length === currentMap.size) {
6268
+ return;
6269
+ }
6270
+ const nextMap = new Map(nextEntries);
6271
+ this.sandboxEffects.set(sandboxId, nextMap);
6272
+ if (nextMap.size === 0) {
6273
+ this.disconnectSandboxEffectHost(sandboxId);
6274
+ this.sandboxEffects.delete(sandboxId);
6275
+ return;
6234
6276
  }
6277
+ await this.syncSandboxEffectCatalog(sandboxId);
6235
6278
  }
6236
6279
  /**
6237
6280
  * Unregister all effects for a sandbox.
@@ -6240,10 +6283,7 @@ var Granular = class {
6240
6283
  const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
6241
6284
  const sandboxId = sandbox.sandboxId;
6242
6285
  this.sandboxEffects.delete(sandboxId);
6243
- const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
6244
- for (const env of activeEnvs) {
6245
- await env.unpublishAllEffects();
6246
- }
6286
+ this.disconnectSandboxEffectHost(sandboxId);
6247
6287
  }
6248
6288
  /**
6249
6289
  * Find a sandbox by name or create it if it doesn't exist
@@ -6279,7 +6319,7 @@ var Granular = class {
6279
6319
  const created = await this.permissionProfiles.create(sandboxId, {
6280
6320
  name: profileName,
6281
6321
  rules: {
6282
- tools: { allow: ["*"] },
6322
+ effects: { allow: ["*"] },
6283
6323
  resources: { allow: ["*"] }
6284
6324
  }
6285
6325
  });