@granular-software/sdk 0.3.4 → 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/README.md +37 -14
- package/dist/adapters/anthropic.d.mts +1 -1
- package/dist/adapters/anthropic.d.ts +1 -1
- package/dist/adapters/langchain.d.mts +1 -1
- package/dist/adapters/langchain.d.ts +1 -1
- package/dist/adapters/mastra.d.mts +1 -1
- package/dist/adapters/mastra.d.ts +1 -1
- package/dist/adapters/openai.d.mts +1 -1
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/cli/index.js +203 -45
- package/dist/index.d.mts +68 -162
- package/dist/index.d.ts +68 -162
- package/dist/index.js +519 -269
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +519 -269
- package/dist/index.mjs.map +1 -1
- package/dist/{types-D5B8WlF4.d.mts → types-C0AVRsVR.d.mts} +92 -31
- package/dist/{types-D5B8WlF4.d.ts → types-C0AVRsVR.d.ts} +92 -31
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3939,6 +3939,9 @@ if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
|
|
|
3939
3939
|
GlobalWebSocket = globalThis.WebSocket;
|
|
3940
3940
|
}
|
|
3941
3941
|
var READY_STATE_OPEN = 1;
|
|
3942
|
+
var TOKEN_REFRESH_LEEWAY_MS = 2 * 60 * 1e3;
|
|
3943
|
+
var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
|
|
3944
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
3942
3945
|
var WSClient = class {
|
|
3943
3946
|
ws = null;
|
|
3944
3947
|
url;
|
|
@@ -3952,6 +3955,7 @@ var WSClient = class {
|
|
|
3952
3955
|
doc = Automerge__namespace.init();
|
|
3953
3956
|
syncState = Automerge__namespace.initSyncState();
|
|
3954
3957
|
reconnectTimer = null;
|
|
3958
|
+
tokenRefreshTimer = null;
|
|
3955
3959
|
isExplicitlyDisconnected = false;
|
|
3956
3960
|
options;
|
|
3957
3961
|
constructor(options) {
|
|
@@ -3960,12 +3964,109 @@ var WSClient = class {
|
|
|
3960
3964
|
this.sessionId = options.sessionId;
|
|
3961
3965
|
this.token = options.token;
|
|
3962
3966
|
}
|
|
3967
|
+
clearTokenRefreshTimer() {
|
|
3968
|
+
if (this.tokenRefreshTimer) {
|
|
3969
|
+
clearTimeout(this.tokenRefreshTimer);
|
|
3970
|
+
this.tokenRefreshTimer = null;
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
decodeBase64Url(payload) {
|
|
3974
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
3975
|
+
const padded = base64 + "=".repeat((4 - (base64.length % 4 || 4)) % 4);
|
|
3976
|
+
if (typeof atob === "function") {
|
|
3977
|
+
return atob(padded);
|
|
3978
|
+
}
|
|
3979
|
+
const maybeBuffer = globalThis.Buffer;
|
|
3980
|
+
if (maybeBuffer) {
|
|
3981
|
+
return maybeBuffer.from(padded, "base64").toString("utf8");
|
|
3982
|
+
}
|
|
3983
|
+
throw new Error("No base64 decoder available");
|
|
3984
|
+
}
|
|
3985
|
+
getTokenExpiryMs(token) {
|
|
3986
|
+
const parts = token.split(".");
|
|
3987
|
+
if (parts.length < 2) {
|
|
3988
|
+
return null;
|
|
3989
|
+
}
|
|
3990
|
+
try {
|
|
3991
|
+
const payloadRaw = this.decodeBase64Url(parts[1]);
|
|
3992
|
+
const payload = JSON.parse(payloadRaw);
|
|
3993
|
+
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
3994
|
+
return null;
|
|
3995
|
+
}
|
|
3996
|
+
return payload.exp * 1e3;
|
|
3997
|
+
} catch {
|
|
3998
|
+
return null;
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
scheduleTokenRefresh() {
|
|
4002
|
+
this.clearTokenRefreshTimer();
|
|
4003
|
+
if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
|
|
4004
|
+
return;
|
|
4005
|
+
}
|
|
4006
|
+
const expiresAt = this.getTokenExpiryMs(this.token);
|
|
4007
|
+
if (!expiresAt) {
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
const refreshInMs = Math.max(1e3, expiresAt - Date.now() - TOKEN_REFRESH_LEEWAY_MS);
|
|
4011
|
+
const delay = Math.min(refreshInMs, MAX_TIMER_DELAY_MS);
|
|
4012
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
4013
|
+
void this.refreshTokenInBackground();
|
|
4014
|
+
}, delay);
|
|
4015
|
+
}
|
|
4016
|
+
async refreshTokenInBackground() {
|
|
4017
|
+
if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
try {
|
|
4021
|
+
const refreshedToken = await this.options.tokenProvider();
|
|
4022
|
+
if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
|
|
4023
|
+
throw new Error("Token provider returned no token");
|
|
4024
|
+
}
|
|
4025
|
+
this.token = refreshedToken;
|
|
4026
|
+
this.scheduleTokenRefresh();
|
|
4027
|
+
} catch (error) {
|
|
4028
|
+
if (this.isExplicitlyDisconnected) {
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
console.warn("[Granular] Token refresh failed, retrying soon:", error);
|
|
4032
|
+
this.clearTokenRefreshTimer();
|
|
4033
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
4034
|
+
void this.refreshTokenInBackground();
|
|
4035
|
+
}, TOKEN_REFRESH_RETRY_MS);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
async resolveTokenForConnect() {
|
|
4039
|
+
if (!this.options.tokenProvider) {
|
|
4040
|
+
return this.token;
|
|
4041
|
+
}
|
|
4042
|
+
const expiresAt = this.getTokenExpiryMs(this.token);
|
|
4043
|
+
const shouldRefresh = expiresAt !== null && expiresAt - Date.now() <= TOKEN_REFRESH_LEEWAY_MS;
|
|
4044
|
+
if (!shouldRefresh) {
|
|
4045
|
+
return this.token;
|
|
4046
|
+
}
|
|
4047
|
+
try {
|
|
4048
|
+
const refreshedToken = await this.options.tokenProvider();
|
|
4049
|
+
if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
|
|
4050
|
+
throw new Error("Token provider returned no token");
|
|
4051
|
+
}
|
|
4052
|
+
this.token = refreshedToken;
|
|
4053
|
+
return refreshedToken;
|
|
4054
|
+
} catch (error) {
|
|
4055
|
+
if (expiresAt > Date.now()) {
|
|
4056
|
+
console.warn("[Granular] Token refresh failed, using current token:", error);
|
|
4057
|
+
return this.token;
|
|
4058
|
+
}
|
|
4059
|
+
throw error;
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
3963
4062
|
/**
|
|
3964
4063
|
* Connect to the WebSocket server
|
|
3965
4064
|
* @returns {Promise<void>} Resolves when connection is open
|
|
3966
4065
|
*/
|
|
3967
4066
|
async connect() {
|
|
4067
|
+
const token = await this.resolveTokenForConnect();
|
|
3968
4068
|
this.isExplicitlyDisconnected = false;
|
|
4069
|
+
this.scheduleTokenRefresh();
|
|
3969
4070
|
if (this.reconnectTimer) {
|
|
3970
4071
|
clearTimeout(this.reconnectTimer);
|
|
3971
4072
|
this.reconnectTimer = null;
|
|
@@ -3985,7 +4086,7 @@ var WSClient = class {
|
|
|
3985
4086
|
try {
|
|
3986
4087
|
const wsUrl = new URL(this.url);
|
|
3987
4088
|
wsUrl.searchParams.set("sessionId", this.sessionId);
|
|
3988
|
-
wsUrl.searchParams.set("token",
|
|
4089
|
+
wsUrl.searchParams.set("token", token);
|
|
3989
4090
|
this.ws = new WebSocketClass(wsUrl.toString());
|
|
3990
4091
|
if (!this.ws) throw new Error("Failed to create WebSocket");
|
|
3991
4092
|
const socket = this.ws;
|
|
@@ -4190,10 +4291,10 @@ var WSClient = class {
|
|
|
4190
4291
|
const snapshotMessage = message;
|
|
4191
4292
|
try {
|
|
4192
4293
|
const bytes = new Uint8Array(snapshotMessage.data);
|
|
4193
|
-
console.log("[Granular DEBUG] Loading snapshot bytes:", bytes.length);
|
|
4294
|
+
console.log("[Granular DEBUG] Loading Automerge session snapshot bytes:", bytes.length);
|
|
4194
4295
|
this.doc = Automerge__namespace.load(bytes);
|
|
4195
4296
|
this.emit("sync", this.doc);
|
|
4196
|
-
console.log("[Granular DEBUG]
|
|
4297
|
+
console.log("[Granular DEBUG] Automerge session snapshot loaded. Doc:", JSON.stringify(Automerge__namespace.toJS(this.doc)));
|
|
4197
4298
|
} catch (e) {
|
|
4198
4299
|
console.warn("[Granular] Failed to load snapshot message", e);
|
|
4199
4300
|
}
|
|
@@ -4356,6 +4457,7 @@ var WSClient = class {
|
|
|
4356
4457
|
clearTimeout(this.reconnectTimer);
|
|
4357
4458
|
this.reconnectTimer = null;
|
|
4358
4459
|
}
|
|
4460
|
+
this.clearTokenRefreshTimer();
|
|
4359
4461
|
if (this.ws) {
|
|
4360
4462
|
this.ws.close(1e3, "Client disconnect");
|
|
4361
4463
|
this.ws = null;
|
|
@@ -4386,6 +4488,17 @@ var Session = class {
|
|
|
4386
4488
|
this.setupEventHandlers();
|
|
4387
4489
|
this.setupToolInvokeHandler();
|
|
4388
4490
|
}
|
|
4491
|
+
buildLegacyEffectContext() {
|
|
4492
|
+
return {
|
|
4493
|
+
effectClientId: this.clientId,
|
|
4494
|
+
sandboxId: "",
|
|
4495
|
+
environmentId: "",
|
|
4496
|
+
sessionId: "",
|
|
4497
|
+
user: {
|
|
4498
|
+
subjectId: ""
|
|
4499
|
+
}
|
|
4500
|
+
};
|
|
4501
|
+
}
|
|
4389
4502
|
// --- Public API ---
|
|
4390
4503
|
get document() {
|
|
4391
4504
|
return this.client.doc;
|
|
@@ -4424,155 +4537,30 @@ var Session = class {
|
|
|
4424
4537
|
});
|
|
4425
4538
|
return result;
|
|
4426
4539
|
}
|
|
4427
|
-
/**
|
|
4428
|
-
* Publish tools to the sandbox and register handlers for reverse-RPC.
|
|
4429
|
-
*
|
|
4430
|
-
* Tools can be:
|
|
4431
|
-
* - **Instance methods**: set `className` (handler receives `(objectId, params)`)
|
|
4432
|
-
* - **Static methods**: set `className` + `static: true` (handler receives `(params)`)
|
|
4433
|
-
* - **Global tools**: omit `className` (handler receives `(params)`)
|
|
4434
|
-
*
|
|
4435
|
-
* Both `inputSchema` and `outputSchema` accept JSON Schema objects. The
|
|
4436
|
-
* `outputSchema` drives the return type in the auto-generated TypeScript
|
|
4437
|
-
* class declarations that sandbox code imports from `./sandbox-tools`.
|
|
4438
|
-
*
|
|
4439
|
-
* This method:
|
|
4440
|
-
* 1. Extracts tool schemas (including `outputSchema`) from the provided tools
|
|
4441
|
-
* 2. Publishes them via `client.publishRawToolCatalog` RPC
|
|
4442
|
-
* 3. Registers handlers locally for `tool.invoke` RPC calls
|
|
4443
|
-
* 4. Returns the `domainRevision` needed for job submission
|
|
4444
|
-
*
|
|
4445
|
-
* @param tools - Array of tools with handlers
|
|
4446
|
-
* @param revision - Optional revision string (default: "1.0.0")
|
|
4447
|
-
* @returns PublishToolsResult with domainRevision
|
|
4448
|
-
*
|
|
4449
|
-
* @example
|
|
4450
|
-
* ```typescript
|
|
4451
|
-
* await env.publishTools([
|
|
4452
|
-
* {
|
|
4453
|
-
* name: 'get_bio',
|
|
4454
|
-
* description: 'Get biography of an author',
|
|
4455
|
-
* className: 'author',
|
|
4456
|
-
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
4457
|
-
* outputSchema: { type: 'object', properties: { bio: { type: 'string' } }, required: ['bio'] },
|
|
4458
|
-
* handler: async (id, params) => ({ bio: `Bio of ${id}` }),
|
|
4459
|
-
* },
|
|
4460
|
-
* ]);
|
|
4461
|
-
* ```
|
|
4462
|
-
*/
|
|
4463
4540
|
async publishTools(tools, revision = "1.0.0") {
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
inputSchema: tool.inputSchema,
|
|
4468
|
-
outputSchema: tool.outputSchema,
|
|
4469
|
-
stability: tool.stability || "stable",
|
|
4470
|
-
provenance: tool.provenance || { source: "mcp" },
|
|
4471
|
-
tags: tool.tags,
|
|
4472
|
-
className: tool.className,
|
|
4473
|
-
static: tool.static
|
|
4474
|
-
}));
|
|
4475
|
-
const result = await this.client.call("client.publishRawToolCatalog", {
|
|
4476
|
-
clientId: this.clientId,
|
|
4477
|
-
revision,
|
|
4478
|
-
tools: schemas
|
|
4479
|
-
});
|
|
4480
|
-
if (!result.accepted || !result.domainRevision) {
|
|
4481
|
-
throw new Error(`Failed to publish tools: ${JSON.stringify(result.rejected)}`);
|
|
4482
|
-
}
|
|
4483
|
-
for (const tool of tools) {
|
|
4484
|
-
this.toolHandlers.set(tool.name, tool.handler);
|
|
4485
|
-
if (tool.className && !tool.static) {
|
|
4486
|
-
this.instanceTools.add(tool.name);
|
|
4487
|
-
} else {
|
|
4488
|
-
this.instanceTools.delete(tool.name);
|
|
4489
|
-
}
|
|
4490
|
-
}
|
|
4491
|
-
this.currentDomainRevision = result.domainRevision;
|
|
4492
|
-
return {
|
|
4493
|
-
accepted: result.accepted,
|
|
4494
|
-
domainRevision: result.domainRevision,
|
|
4495
|
-
rejected: result.rejected
|
|
4496
|
-
};
|
|
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
|
+
);
|
|
4497
4544
|
}
|
|
4498
|
-
/**
|
|
4499
|
-
* Publish a single effect (tool) to the sandbox.
|
|
4500
|
-
*
|
|
4501
|
-
* Adds the effect to the local registry and re-publishes the
|
|
4502
|
-
* full tool catalog to the server. If an effect with the same
|
|
4503
|
-
* name already exists, it is replaced.
|
|
4504
|
-
*
|
|
4505
|
-
* @param effect - The effect (tool) to publish
|
|
4506
|
-
* @returns PublishToolsResult with domainRevision
|
|
4507
|
-
*
|
|
4508
|
-
* @example
|
|
4509
|
-
* ```typescript
|
|
4510
|
-
* await env.publishEffect({
|
|
4511
|
-
* name: 'get_bio',
|
|
4512
|
-
* description: 'Get biography of an author',
|
|
4513
|
-
* className: 'author',
|
|
4514
|
-
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
4515
|
-
* handler: async (id, params) => ({ bio: `Bio of ${id}` }),
|
|
4516
|
-
* });
|
|
4517
|
-
* ```
|
|
4518
|
-
*/
|
|
4519
4545
|
async publishEffect(effect) {
|
|
4520
|
-
|
|
4521
|
-
|
|
4546
|
+
throw new Error(
|
|
4547
|
+
"Environment-scoped effect publication was removed. Use granular.registerEffect(environment.sandboxId, effect)."
|
|
4548
|
+
);
|
|
4522
4549
|
}
|
|
4523
|
-
/**
|
|
4524
|
-
* Publish multiple effects (tools) at once.
|
|
4525
|
-
*
|
|
4526
|
-
* Adds all effects to the local registry and re-publishes the
|
|
4527
|
-
* full tool catalog in a single RPC call.
|
|
4528
|
-
*
|
|
4529
|
-
* @param effects - Array of effects to publish
|
|
4530
|
-
* @returns PublishToolsResult with domainRevision
|
|
4531
|
-
*
|
|
4532
|
-
* @example
|
|
4533
|
-
* ```typescript
|
|
4534
|
-
* await env.publishEffects([
|
|
4535
|
-
* { name: 'get_bio', description: '...', inputSchema: {}, handler: async (id) => ({}) },
|
|
4536
|
-
* { name: 'search', description: '...', inputSchema: {}, handler: async (params) => ({}) },
|
|
4537
|
-
* ]);
|
|
4538
|
-
* ```
|
|
4539
|
-
*/
|
|
4540
4550
|
async publishEffects(effects) {
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
return this._syncEffects();
|
|
4551
|
+
throw new Error(
|
|
4552
|
+
"Environment-scoped effect publication was removed. Use granular.registerEffects(environment.sandboxId, effects)."
|
|
4553
|
+
);
|
|
4545
4554
|
}
|
|
4546
|
-
/**
|
|
4547
|
-
* Remove an effect by name and re-publish the remaining catalog.
|
|
4548
|
-
*
|
|
4549
|
-
* @param name - The name of the effect to remove
|
|
4550
|
-
* @returns PublishToolsResult with domainRevision
|
|
4551
|
-
*/
|
|
4552
4555
|
async unpublishEffect(name) {
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
return this._syncEffects();
|
|
4556
|
+
throw new Error(
|
|
4557
|
+
"Environment-scoped effect publication was removed. Use granular.unregisterEffect(environment.sandboxId, effectName)."
|
|
4558
|
+
);
|
|
4557
4559
|
}
|
|
4558
|
-
/**
|
|
4559
|
-
* Remove all effects and publish an empty catalog.
|
|
4560
|
-
*
|
|
4561
|
-
* @returns PublishToolsResult with domainRevision
|
|
4562
|
-
*/
|
|
4563
4560
|
async unpublishAllEffects() {
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
return this._syncEffects();
|
|
4568
|
-
}
|
|
4569
|
-
/**
|
|
4570
|
-
* Internal: re-publish the full effect catalog to the server.
|
|
4571
|
-
* Called after any mutation to the effects Map.
|
|
4572
|
-
*/
|
|
4573
|
-
async _syncEffects() {
|
|
4574
|
-
const allEffects = Array.from(this.effects.values());
|
|
4575
|
-
return this.publishTools(allEffects);
|
|
4561
|
+
throw new Error(
|
|
4562
|
+
"Environment-scoped effect publication was removed. Use granular.unregisterAllEffects(environment.sandboxId)."
|
|
4563
|
+
);
|
|
4576
4564
|
}
|
|
4577
4565
|
/**
|
|
4578
4566
|
* Submit a job to execute code in the sandbox.
|
|
@@ -4586,14 +4574,14 @@ var Session = class {
|
|
|
4586
4574
|
* const books = await tolkien.get_books();
|
|
4587
4575
|
* ```
|
|
4588
4576
|
*
|
|
4589
|
-
*
|
|
4590
|
-
* `
|
|
4577
|
+
* Effect calls (instance methods, static methods, global functions) trigger
|
|
4578
|
+
* `effect.invoke` RPC back to the sandbox effect host, where the registered handlers
|
|
4591
4579
|
* execute locally and return the result to the sandbox.
|
|
4592
4580
|
*/
|
|
4593
4581
|
async submitJob(code, domainRevision) {
|
|
4594
4582
|
const revision = domainRevision || this.currentDomainRevision || this.client.doc?.domain?.active || void 0;
|
|
4595
4583
|
if (!revision) {
|
|
4596
|
-
throw new Error("No domain revision available.
|
|
4584
|
+
throw new Error("No domain revision available. Register live effects or ensure the build schema is activated.");
|
|
4597
4585
|
}
|
|
4598
4586
|
const result = await this.client.call("job.submit", {
|
|
4599
4587
|
domainRevision: revision,
|
|
@@ -4625,10 +4613,10 @@ var Session = class {
|
|
|
4625
4613
|
await this.client.call("prompt.answer", { promptId, value: answer });
|
|
4626
4614
|
}
|
|
4627
4615
|
/**
|
|
4628
|
-
* Get the current list of available
|
|
4629
|
-
* Consolidates
|
|
4616
|
+
* Get the current list of available effects.
|
|
4617
|
+
* Consolidates effect declarations and live availability for the session.
|
|
4630
4618
|
*/
|
|
4631
|
-
|
|
4619
|
+
getEffects() {
|
|
4632
4620
|
const doc = this.client.doc;
|
|
4633
4621
|
const toolMap = /* @__PURE__ */ new Map();
|
|
4634
4622
|
const domainPkg = doc.domain?.packages?.domain;
|
|
@@ -4673,10 +4661,32 @@ var Session = class {
|
|
|
4673
4661
|
return Array.from(toolMap.values());
|
|
4674
4662
|
}
|
|
4675
4663
|
/**
|
|
4676
|
-
*
|
|
4664
|
+
* Backwards-compatible alias for `getEffects()`.
|
|
4665
|
+
*/
|
|
4666
|
+
getTools() {
|
|
4667
|
+
return this.getEffects();
|
|
4668
|
+
}
|
|
4669
|
+
/**
|
|
4670
|
+
* Subscribe to effect changes (added, removed, updated).
|
|
4677
4671
|
* @param callback - Function called with change events
|
|
4678
4672
|
* @returns Unsubscribe function
|
|
4679
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
|
+
*/
|
|
4680
4690
|
onToolsChanged(callback) {
|
|
4681
4691
|
const handler = (data) => callback(data);
|
|
4682
4692
|
if (!this.eventListeners.has("tools:changed")) {
|
|
@@ -4789,7 +4799,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4789
4799
|
}
|
|
4790
4800
|
}
|
|
4791
4801
|
if (globalTools && globalTools.length > 0) {
|
|
4792
|
-
docs2 += "## Global
|
|
4802
|
+
docs2 += "## Global Effects\n\n";
|
|
4793
4803
|
for (const tool of globalTools) {
|
|
4794
4804
|
docs2 += `### ${tool.name}
|
|
4795
4805
|
|
|
@@ -4807,10 +4817,10 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4807
4817
|
return docs2;
|
|
4808
4818
|
}
|
|
4809
4819
|
if (!tools || tools.length === 0) {
|
|
4810
|
-
return "No
|
|
4820
|
+
return "No effects available in this domain.";
|
|
4811
4821
|
}
|
|
4812
|
-
let docs = "# Available
|
|
4813
|
-
docs += "Import
|
|
4822
|
+
let docs = "# Available Effects\n\n";
|
|
4823
|
+
docs += "Import effects from `./sandbox-tools` and call them with await:\n\n";
|
|
4814
4824
|
docs += '```typescript\nimport { tools } from "./sandbox-tools";\n\n';
|
|
4815
4825
|
docs += "// Example:\n";
|
|
4816
4826
|
docs += `const result = await tools.${tools[0]?.name || "example"}(input);
|
|
@@ -4842,6 +4852,13 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4842
4852
|
* Close the session and disconnect from the sandbox
|
|
4843
4853
|
*/
|
|
4844
4854
|
async disconnect() {
|
|
4855
|
+
try {
|
|
4856
|
+
await this.client.call("client.goodbye", {
|
|
4857
|
+
clientId: this.clientId,
|
|
4858
|
+
timestamp: Date.now()
|
|
4859
|
+
});
|
|
4860
|
+
} catch (error) {
|
|
4861
|
+
}
|
|
4845
4862
|
this.client.disconnect();
|
|
4846
4863
|
}
|
|
4847
4864
|
// --- Event Handling ---
|
|
@@ -4867,6 +4884,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4867
4884
|
setupToolInvokeHandler() {
|
|
4868
4885
|
this.client.registerRpcHandler("tool.invoke", async (params) => {
|
|
4869
4886
|
const { callId, toolName, input } = params;
|
|
4887
|
+
this.emit("effect:invoke", { callId, effectKey: toolName, toolName, input });
|
|
4870
4888
|
this.emit("tool:invoke", { callId, toolName, input });
|
|
4871
4889
|
const handler = this.toolHandlers.get(toolName);
|
|
4872
4890
|
if (!handler) {
|
|
@@ -4878,12 +4896,14 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4878
4896
|
}
|
|
4879
4897
|
try {
|
|
4880
4898
|
let result;
|
|
4899
|
+
const invocationContext = this.buildLegacyEffectContext();
|
|
4881
4900
|
if (this.instanceTools.has(toolName) && input && typeof input === "object" && "_objectId" in input) {
|
|
4882
4901
|
const { _objectId, ...restParams } = input;
|
|
4883
|
-
result = await handler(_objectId, restParams);
|
|
4902
|
+
result = await handler(_objectId, restParams, invocationContext);
|
|
4884
4903
|
} else {
|
|
4885
|
-
result = await handler(input);
|
|
4904
|
+
result = await handler(input, invocationContext);
|
|
4886
4905
|
}
|
|
4906
|
+
this.emit("effect:result", { callId, effectKey: toolName, result });
|
|
4887
4907
|
this.emit("tool:result", { callId, result });
|
|
4888
4908
|
await this.client.call("tool.result", {
|
|
4889
4909
|
callId,
|
|
@@ -4891,6 +4911,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4891
4911
|
});
|
|
4892
4912
|
} catch (error) {
|
|
4893
4913
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4914
|
+
this.emit("effect:result", { callId, effectKey: toolName, error: errorMessage });
|
|
4894
4915
|
this.emit("tool:result", { callId, error: errorMessage });
|
|
4895
4916
|
await this.client.call("tool.result", {
|
|
4896
4917
|
callId,
|
|
@@ -4923,10 +4944,10 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4923
4944
|
}
|
|
4924
4945
|
}
|
|
4925
4946
|
/**
|
|
4926
|
-
* Check for changes in the
|
|
4947
|
+
* Check for changes in the effect catalog and emit change events if needed.
|
|
4927
4948
|
*/
|
|
4928
4949
|
checkForToolChanges() {
|
|
4929
|
-
const currentTools = this.
|
|
4950
|
+
const currentTools = this.getEffects();
|
|
4930
4951
|
const currentMap = new Map(currentTools.map((t) => [t.name, t]));
|
|
4931
4952
|
const added = [];
|
|
4932
4953
|
const removed = [];
|
|
@@ -4942,6 +4963,12 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4942
4963
|
}
|
|
4943
4964
|
if (added.length > 0 || removed.length > 0) {
|
|
4944
4965
|
this.lastKnownTools = currentMap;
|
|
4966
|
+
this.emit("effects:changed", {
|
|
4967
|
+
effects: currentTools,
|
|
4968
|
+
tools: currentTools,
|
|
4969
|
+
added,
|
|
4970
|
+
removed
|
|
4971
|
+
});
|
|
4945
4972
|
this.emit("tools:changed", {
|
|
4946
4973
|
tools: currentTools,
|
|
4947
4974
|
added,
|
|
@@ -5052,6 +5079,50 @@ var JobImplementation = class {
|
|
|
5052
5079
|
}
|
|
5053
5080
|
};
|
|
5054
5081
|
|
|
5082
|
+
// src/endpoints.ts
|
|
5083
|
+
var LOCAL_API_URL = "ws://localhost:8787/granular";
|
|
5084
|
+
var PRODUCTION_API_URL = "wss://api.granular.dev/v2/ws";
|
|
5085
|
+
function readEnv(name) {
|
|
5086
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
5087
|
+
return process.env[name];
|
|
5088
|
+
}
|
|
5089
|
+
function normalizeMode(value) {
|
|
5090
|
+
if (!value) return void 0;
|
|
5091
|
+
const normalized = value.trim().toLowerCase();
|
|
5092
|
+
if (normalized === "local") return "local";
|
|
5093
|
+
if (normalized === "prod" || normalized === "production") return "production";
|
|
5094
|
+
if (normalized === "auto") return "auto";
|
|
5095
|
+
return void 0;
|
|
5096
|
+
}
|
|
5097
|
+
function isTruthy(value) {
|
|
5098
|
+
if (!value) return false;
|
|
5099
|
+
const normalized = value.trim().toLowerCase();
|
|
5100
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
5101
|
+
}
|
|
5102
|
+
function resolveEndpointMode(explicitMode) {
|
|
5103
|
+
const explicit = normalizeMode(explicitMode);
|
|
5104
|
+
if (explicit === "local" || explicit === "production") {
|
|
5105
|
+
return explicit;
|
|
5106
|
+
}
|
|
5107
|
+
const envMode = normalizeMode(readEnv("GRANULAR_ENDPOINT_MODE") || readEnv("GRANULAR_ENV"));
|
|
5108
|
+
if (envMode === "local" || envMode === "production") {
|
|
5109
|
+
return envMode;
|
|
5110
|
+
}
|
|
5111
|
+
if (isTruthy(readEnv("GRANULAR_USE_LOCAL_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_LOCAL"))) {
|
|
5112
|
+
return "local";
|
|
5113
|
+
}
|
|
5114
|
+
if (isTruthy(readEnv("GRANULAR_USE_PRODUCTION_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_PROD"))) {
|
|
5115
|
+
return "production";
|
|
5116
|
+
}
|
|
5117
|
+
return readEnv("NODE_ENV") === "development" ? "local" : "production";
|
|
5118
|
+
}
|
|
5119
|
+
function resolveApiUrl(explicitApiUrl, mode) {
|
|
5120
|
+
if (explicitApiUrl) {
|
|
5121
|
+
return explicitApiUrl;
|
|
5122
|
+
}
|
|
5123
|
+
return resolveEndpointMode(mode) === "local" ? LOCAL_API_URL : PRODUCTION_API_URL;
|
|
5124
|
+
}
|
|
5125
|
+
|
|
5055
5126
|
// src/client.ts
|
|
5056
5127
|
var STANDARD_MODULES_OPERATIONS = [
|
|
5057
5128
|
{ create: "entity", has: { id: { value: "auto-generated" }, createdAt: { value: void 0 } } },
|
|
@@ -5066,6 +5137,37 @@ var STANDARD_MODULES_OPERATIONS = [
|
|
|
5066
5137
|
var BUILTIN_MODULES = {
|
|
5067
5138
|
"standard_modules": STANDARD_MODULES_OPERATIONS
|
|
5068
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
|
+
}
|
|
5069
5171
|
var Environment = class _Environment extends Session {
|
|
5070
5172
|
envData;
|
|
5071
5173
|
_apiKey;
|
|
@@ -5096,6 +5198,60 @@ var Environment = class _Environment extends Session {
|
|
|
5096
5198
|
get apiEndpoint() {
|
|
5097
5199
|
return this._apiEndpoint;
|
|
5098
5200
|
}
|
|
5201
|
+
getRuntimeBaseUrl() {
|
|
5202
|
+
try {
|
|
5203
|
+
const endpoint = new URL(this._apiEndpoint);
|
|
5204
|
+
const graphqlSuffix = "/orchestrator/graphql";
|
|
5205
|
+
if (endpoint.pathname.endsWith(graphqlSuffix)) {
|
|
5206
|
+
endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
|
|
5207
|
+
} else if (endpoint.pathname.endsWith("/graphql")) {
|
|
5208
|
+
endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
|
|
5209
|
+
}
|
|
5210
|
+
endpoint.search = "";
|
|
5211
|
+
endpoint.hash = "";
|
|
5212
|
+
return endpoint.toString().replace(/\/$/, "");
|
|
5213
|
+
} catch {
|
|
5214
|
+
return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
/**
|
|
5218
|
+
* Close the session and disconnect from the sandbox.
|
|
5219
|
+
*
|
|
5220
|
+
* Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
|
|
5221
|
+
* to the runtime goodbye endpoint if no definitive WS-side runtime notify
|
|
5222
|
+
* acknowledgement was observed.
|
|
5223
|
+
*/
|
|
5224
|
+
async disconnect() {
|
|
5225
|
+
let wsNotifiedRuntime = false;
|
|
5226
|
+
try {
|
|
5227
|
+
const goodbye = await this.rpc("client.goodbye", {
|
|
5228
|
+
timestamp: Date.now()
|
|
5229
|
+
});
|
|
5230
|
+
wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
|
|
5231
|
+
} catch {
|
|
5232
|
+
wsNotifiedRuntime = false;
|
|
5233
|
+
}
|
|
5234
|
+
if (!wsNotifiedRuntime) {
|
|
5235
|
+
try {
|
|
5236
|
+
const runtimeBase = this.getRuntimeBaseUrl();
|
|
5237
|
+
await fetch(
|
|
5238
|
+
`${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
|
|
5239
|
+
{
|
|
5240
|
+
method: "POST",
|
|
5241
|
+
headers: {
|
|
5242
|
+
"Content-Type": "application/json",
|
|
5243
|
+
"Authorization": `Bearer ${this._apiKey}`
|
|
5244
|
+
},
|
|
5245
|
+
body: JSON.stringify({
|
|
5246
|
+
reason: "sdk_disconnect_http_fallback"
|
|
5247
|
+
})
|
|
5248
|
+
}
|
|
5249
|
+
);
|
|
5250
|
+
} catch {
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
this.client.disconnect();
|
|
5254
|
+
}
|
|
5099
5255
|
// ==================== GRAPH CONTAINER READINESS ====================
|
|
5100
5256
|
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
5101
5257
|
graphContainerStatus = null;
|
|
@@ -5756,72 +5912,31 @@ var Environment = class _Environment extends Session {
|
|
|
5756
5912
|
}
|
|
5757
5913
|
// ==================== PUBLISH TOOLS ====================
|
|
5758
5914
|
/**
|
|
5759
|
-
*
|
|
5760
|
-
* This is the main entry point for setting up tools.
|
|
5761
|
-
*
|
|
5762
|
-
* @example
|
|
5763
|
-
* ```typescript
|
|
5764
|
-
* const tools = [
|
|
5765
|
-
* {
|
|
5766
|
-
* name: 'get_weather',
|
|
5767
|
-
* description: 'Get weather for a city',
|
|
5768
|
-
* inputSchema: { type: 'object', properties: { city: { type: 'string' } } },
|
|
5769
|
-
* handler: async ({ city }) => ({ temp: 22 }),
|
|
5770
|
-
* },
|
|
5771
|
-
* ];
|
|
5772
|
-
*
|
|
5773
|
-
* const { domainRevision } = await environment.publishTools(tools);
|
|
5774
|
-
*
|
|
5775
|
-
* // Now submit jobs that use those tools
|
|
5776
|
-
* const job = await environment.submitJob(`
|
|
5777
|
-
* import { Author } from './sandbox-tools';
|
|
5778
|
-
* const weather = await Author.get_weather({ city: 'Paris' });
|
|
5779
|
-
* return weather;
|
|
5780
|
-
* `);
|
|
5781
|
-
*
|
|
5782
|
-
* const result = await job.result;
|
|
5783
|
-
* ```
|
|
5915
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5784
5916
|
*/
|
|
5785
5917
|
async publishTools(tools, revision = "1.0.0") {
|
|
5786
5918
|
return super.publishTools(tools, revision);
|
|
5787
5919
|
}
|
|
5788
5920
|
/**
|
|
5789
|
-
*
|
|
5790
|
-
*
|
|
5791
|
-
* Adds the effect to the local registry and re-publishes the
|
|
5792
|
-
* full tool catalog to the server. If an effect with the same
|
|
5793
|
-
* name already exists, it is replaced.
|
|
5794
|
-
*
|
|
5795
|
-
* @example
|
|
5796
|
-
* ```typescript
|
|
5797
|
-
* await env.publishEffect({
|
|
5798
|
-
* name: 'get_bio',
|
|
5799
|
-
* description: 'Get biography',
|
|
5800
|
-
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
5801
|
-
* handler: async (params) => ({ bio: 'Hello' }),
|
|
5802
|
-
* });
|
|
5803
|
-
* ```
|
|
5921
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5804
5922
|
*/
|
|
5805
5923
|
async publishEffect(effect) {
|
|
5806
5924
|
return super.publishEffect(effect);
|
|
5807
5925
|
}
|
|
5808
5926
|
/**
|
|
5809
|
-
*
|
|
5810
|
-
*
|
|
5811
|
-
* Adds all effects to the local registry and re-publishes the
|
|
5812
|
-
* full tool catalog in a single RPC call.
|
|
5927
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5813
5928
|
*/
|
|
5814
5929
|
async publishEffects(effects) {
|
|
5815
5930
|
return super.publishEffects(effects);
|
|
5816
5931
|
}
|
|
5817
5932
|
/**
|
|
5818
|
-
*
|
|
5933
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5819
5934
|
*/
|
|
5820
5935
|
async unpublishEffect(name) {
|
|
5821
5936
|
return super.unpublishEffect(name);
|
|
5822
5937
|
}
|
|
5823
5938
|
/**
|
|
5824
|
-
*
|
|
5939
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5825
5940
|
*/
|
|
5826
5941
|
async unpublishAllEffects() {
|
|
5827
5942
|
return super.unpublishAllEffects();
|
|
@@ -5831,13 +5946,16 @@ var Granular = class {
|
|
|
5831
5946
|
apiKey;
|
|
5832
5947
|
apiUrl;
|
|
5833
5948
|
httpUrl;
|
|
5949
|
+
tokenProvider;
|
|
5834
5950
|
WebSocketCtor;
|
|
5835
5951
|
onUnexpectedClose;
|
|
5836
5952
|
onReconnectError;
|
|
5837
|
-
/** Sandbox-level effect registry: sandboxId → (
|
|
5953
|
+
/** Sandbox-level effect registry: sandboxId → (effectKey → ToolWithHandler) */
|
|
5838
5954
|
sandboxEffects = /* @__PURE__ */ new Map();
|
|
5839
|
-
/**
|
|
5840
|
-
|
|
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();
|
|
5841
5959
|
/**
|
|
5842
5960
|
* Create a new Granular client
|
|
5843
5961
|
* @param options - Client configuration
|
|
@@ -5848,11 +5966,12 @@ var Granular = class {
|
|
|
5848
5966
|
throw new Error("Granular client requires either apiKey or token. Set GRANULAR_API_KEY or GRANULAR_TOKEN, or pass one in options.");
|
|
5849
5967
|
}
|
|
5850
5968
|
this.apiKey = auth;
|
|
5851
|
-
this.apiUrl = options.apiUrl
|
|
5969
|
+
this.apiUrl = resolveApiUrl(options.apiUrl, options.endpointMode);
|
|
5970
|
+
this.tokenProvider = options.tokenProvider;
|
|
5852
5971
|
this.WebSocketCtor = options.WebSocketCtor;
|
|
5853
5972
|
this.onUnexpectedClose = options.onUnexpectedClose;
|
|
5854
5973
|
this.onReconnectError = options.onReconnectError;
|
|
5855
|
-
this.httpUrl = this.apiUrl.replace("
|
|
5974
|
+
this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
|
|
5856
5975
|
}
|
|
5857
5976
|
/**
|
|
5858
5977
|
* Records/upserts a user and prepares them for sandbox connections
|
|
@@ -5889,8 +6008,9 @@ var Granular = class {
|
|
|
5889
6008
|
/**
|
|
5890
6009
|
* Connect to a sandbox and establish a real-time environment session.
|
|
5891
6010
|
*
|
|
5892
|
-
*
|
|
5893
|
-
*
|
|
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.
|
|
5894
6014
|
*
|
|
5895
6015
|
* @param options - Connection options
|
|
5896
6016
|
* @returns An active environment session
|
|
@@ -5907,10 +6027,12 @@ var Granular = class {
|
|
|
5907
6027
|
* user,
|
|
5908
6028
|
* });
|
|
5909
6029
|
*
|
|
5910
|
-
*
|
|
5911
|
-
*
|
|
5912
|
-
*
|
|
5913
|
-
*
|
|
6030
|
+
* await granular.registerEffect('my-sandbox', {
|
|
6031
|
+
* name: 'greet',
|
|
6032
|
+
* description: 'Say hello',
|
|
6033
|
+
* inputSchema: { type: 'object', properties: {} },
|
|
6034
|
+
* handler: async () => 'Hello!',
|
|
6035
|
+
* });
|
|
5914
6036
|
*
|
|
5915
6037
|
* // Submit job
|
|
5916
6038
|
* const job = await environment.submitJob(`
|
|
@@ -5932,10 +6054,19 @@ var Granular = class {
|
|
|
5932
6054
|
subjectId: options.user.subjectId,
|
|
5933
6055
|
permissionProfileId: null
|
|
5934
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
|
+
});
|
|
5935
6065
|
const client = new WSClient({
|
|
5936
|
-
url:
|
|
5937
|
-
sessionId:
|
|
5938
|
-
token:
|
|
6066
|
+
url: session.wsUrl,
|
|
6067
|
+
sessionId: session.sessionId,
|
|
6068
|
+
token: session.token,
|
|
6069
|
+
tokenProvider: this.tokenProvider,
|
|
5939
6070
|
WebSocketCtor: this.WebSocketCtor,
|
|
5940
6071
|
onUnexpectedClose: this.onUnexpectedClose,
|
|
5941
6072
|
onReconnectError: this.onReconnectError
|
|
@@ -5943,47 +6074,165 @@ var Granular = class {
|
|
|
5943
6074
|
await client.connect();
|
|
5944
6075
|
const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
|
|
5945
6076
|
const environment = new Environment(client, envData, clientId, this.apiKey, graphqlEndpoint);
|
|
5946
|
-
if (!this.activeEnvironments.has(sandbox.sandboxId)) {
|
|
5947
|
-
this.activeEnvironments.set(sandbox.sandboxId, []);
|
|
5948
|
-
}
|
|
5949
|
-
this.activeEnvironments.get(sandbox.sandboxId).push(environment);
|
|
5950
|
-
environment.on("disconnect", () => {
|
|
5951
|
-
const list = this.activeEnvironments.get(sandbox.sandboxId);
|
|
5952
|
-
if (list) {
|
|
5953
|
-
this.activeEnvironments.set(sandbox.sandboxId, list.filter((e) => e !== environment));
|
|
5954
|
-
}
|
|
5955
|
-
});
|
|
5956
6077
|
await environment.hello();
|
|
5957
|
-
const effects = this.sandboxEffects.get(sandbox.sandboxId);
|
|
5958
|
-
if (effects && effects.size > 0) {
|
|
5959
|
-
const effectsList = Array.from(effects.values());
|
|
5960
|
-
console.log(`[Granular] Auto-publishing ${effectsList.length} effects for sandbox ${sandbox.sandboxId}`);
|
|
5961
|
-
await environment.publishEffects(effectsList);
|
|
5962
|
-
}
|
|
5963
6078
|
return environment;
|
|
5964
6079
|
}
|
|
6080
|
+
async activateEnvironment(environmentId) {
|
|
6081
|
+
await this.request(`/orchestrator/runtime/environments/${environmentId}/activate`, {
|
|
6082
|
+
method: "POST",
|
|
6083
|
+
body: JSON.stringify({})
|
|
6084
|
+
});
|
|
6085
|
+
}
|
|
5965
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
|
+
}
|
|
5966
6225
|
/**
|
|
5967
6226
|
* Register an effect (tool) for a specific sandbox.
|
|
5968
|
-
*
|
|
5969
|
-
* The effect will be automatically published to:
|
|
5970
|
-
* 1. Any currently active environments for this sandbox
|
|
5971
|
-
* 2. Any new environments created/connected for this sandbox
|
|
5972
|
-
*
|
|
6227
|
+
*
|
|
5973
6228
|
* @param sandboxNameOrId - The name or ID of the sandbox
|
|
5974
6229
|
* @param effect - The tool definition and handler
|
|
5975
6230
|
*/
|
|
5976
6231
|
async registerEffect(sandboxNameOrId, effect) {
|
|
5977
6232
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
5978
6233
|
const sandboxId = sandbox.sandboxId;
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
}
|
|
5982
|
-
this.sandboxEffects.get(sandboxId).set(effect.name, effect);
|
|
5983
|
-
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
5984
|
-
for (const env of activeEnvs) {
|
|
5985
|
-
await env.publishEffect(effect);
|
|
5986
|
-
}
|
|
6234
|
+
this.getSandboxEffectMap(sandboxId).set(computeEffectKey(effect), effect);
|
|
6235
|
+
await this.syncSandboxEffectCatalog(sandboxId);
|
|
5987
6236
|
}
|
|
5988
6237
|
/**
|
|
5989
6238
|
* Register multiple effects (tools) for a specific sandbox.
|
|
@@ -5993,35 +6242,39 @@ var Granular = class {
|
|
|
5993
6242
|
async registerEffects(sandboxNameOrId, effects) {
|
|
5994
6243
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
5995
6244
|
const sandboxId = sandbox.sandboxId;
|
|
5996
|
-
|
|
5997
|
-
this.sandboxEffects.set(sandboxId, /* @__PURE__ */ new Map());
|
|
5998
|
-
}
|
|
5999
|
-
const map = this.sandboxEffects.get(sandboxId);
|
|
6245
|
+
const map = this.getSandboxEffectMap(sandboxId);
|
|
6000
6246
|
for (const effect of effects) {
|
|
6001
|
-
map.set(effect
|
|
6002
|
-
}
|
|
6003
|
-
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
6004
|
-
for (const env of activeEnvs) {
|
|
6005
|
-
await env.publishEffects(effects);
|
|
6247
|
+
map.set(computeEffectKey(effect), effect);
|
|
6006
6248
|
}
|
|
6249
|
+
await this.syncSandboxEffectCatalog(sandboxId);
|
|
6007
6250
|
}
|
|
6008
6251
|
/**
|
|
6009
6252
|
* Unregister an effect from a sandbox.
|
|
6010
6253
|
*
|
|
6011
|
-
* Removes it from the local registry and
|
|
6012
|
-
*
|
|
6254
|
+
* Removes it from the local sandbox registry and updates the
|
|
6255
|
+
* sandbox-scoped live catalog.
|
|
6013
6256
|
*/
|
|
6014
6257
|
async unregisterEffect(sandboxNameOrId, name) {
|
|
6015
6258
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
6016
6259
|
const sandboxId = sandbox.sandboxId;
|
|
6017
|
-
const
|
|
6018
|
-
if (
|
|
6019
|
-
|
|
6260
|
+
const currentMap = this.sandboxEffects.get(sandboxId);
|
|
6261
|
+
if (!currentMap) {
|
|
6262
|
+
return;
|
|
6020
6263
|
}
|
|
6021
|
-
const
|
|
6022
|
-
|
|
6023
|
-
|
|
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;
|
|
6024
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;
|
|
6276
|
+
}
|
|
6277
|
+
await this.syncSandboxEffectCatalog(sandboxId);
|
|
6025
6278
|
}
|
|
6026
6279
|
/**
|
|
6027
6280
|
* Unregister all effects for a sandbox.
|
|
@@ -6030,10 +6283,7 @@ var Granular = class {
|
|
|
6030
6283
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
6031
6284
|
const sandboxId = sandbox.sandboxId;
|
|
6032
6285
|
this.sandboxEffects.delete(sandboxId);
|
|
6033
|
-
|
|
6034
|
-
for (const env of activeEnvs) {
|
|
6035
|
-
await env.unpublishAllEffects();
|
|
6036
|
-
}
|
|
6286
|
+
this.disconnectSandboxEffectHost(sandboxId);
|
|
6037
6287
|
}
|
|
6038
6288
|
/**
|
|
6039
6289
|
* Find a sandbox by name or create it if it doesn't exist
|
|
@@ -6069,7 +6319,7 @@ var Granular = class {
|
|
|
6069
6319
|
const created = await this.permissionProfiles.create(sandboxId, {
|
|
6070
6320
|
name: profileName,
|
|
6071
6321
|
rules: {
|
|
6072
|
-
|
|
6322
|
+
effects: { allow: ["*"] },
|
|
6073
6323
|
resources: { allow: ["*"] }
|
|
6074
6324
|
}
|
|
6075
6325
|
});
|