@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.mjs
CHANGED
|
@@ -3917,6 +3917,9 @@ if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
|
|
|
3917
3917
|
GlobalWebSocket = globalThis.WebSocket;
|
|
3918
3918
|
}
|
|
3919
3919
|
var READY_STATE_OPEN = 1;
|
|
3920
|
+
var TOKEN_REFRESH_LEEWAY_MS = 2 * 60 * 1e3;
|
|
3921
|
+
var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
|
|
3922
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
3920
3923
|
var WSClient = class {
|
|
3921
3924
|
ws = null;
|
|
3922
3925
|
url;
|
|
@@ -3930,6 +3933,7 @@ var WSClient = class {
|
|
|
3930
3933
|
doc = Automerge.init();
|
|
3931
3934
|
syncState = Automerge.initSyncState();
|
|
3932
3935
|
reconnectTimer = null;
|
|
3936
|
+
tokenRefreshTimer = null;
|
|
3933
3937
|
isExplicitlyDisconnected = false;
|
|
3934
3938
|
options;
|
|
3935
3939
|
constructor(options) {
|
|
@@ -3938,12 +3942,109 @@ var WSClient = class {
|
|
|
3938
3942
|
this.sessionId = options.sessionId;
|
|
3939
3943
|
this.token = options.token;
|
|
3940
3944
|
}
|
|
3945
|
+
clearTokenRefreshTimer() {
|
|
3946
|
+
if (this.tokenRefreshTimer) {
|
|
3947
|
+
clearTimeout(this.tokenRefreshTimer);
|
|
3948
|
+
this.tokenRefreshTimer = null;
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
decodeBase64Url(payload) {
|
|
3952
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
3953
|
+
const padded = base64 + "=".repeat((4 - (base64.length % 4 || 4)) % 4);
|
|
3954
|
+
if (typeof atob === "function") {
|
|
3955
|
+
return atob(padded);
|
|
3956
|
+
}
|
|
3957
|
+
const maybeBuffer = globalThis.Buffer;
|
|
3958
|
+
if (maybeBuffer) {
|
|
3959
|
+
return maybeBuffer.from(padded, "base64").toString("utf8");
|
|
3960
|
+
}
|
|
3961
|
+
throw new Error("No base64 decoder available");
|
|
3962
|
+
}
|
|
3963
|
+
getTokenExpiryMs(token) {
|
|
3964
|
+
const parts = token.split(".");
|
|
3965
|
+
if (parts.length < 2) {
|
|
3966
|
+
return null;
|
|
3967
|
+
}
|
|
3968
|
+
try {
|
|
3969
|
+
const payloadRaw = this.decodeBase64Url(parts[1]);
|
|
3970
|
+
const payload = JSON.parse(payloadRaw);
|
|
3971
|
+
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
3972
|
+
return null;
|
|
3973
|
+
}
|
|
3974
|
+
return payload.exp * 1e3;
|
|
3975
|
+
} catch {
|
|
3976
|
+
return null;
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
scheduleTokenRefresh() {
|
|
3980
|
+
this.clearTokenRefreshTimer();
|
|
3981
|
+
if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3984
|
+
const expiresAt = this.getTokenExpiryMs(this.token);
|
|
3985
|
+
if (!expiresAt) {
|
|
3986
|
+
return;
|
|
3987
|
+
}
|
|
3988
|
+
const refreshInMs = Math.max(1e3, expiresAt - Date.now() - TOKEN_REFRESH_LEEWAY_MS);
|
|
3989
|
+
const delay = Math.min(refreshInMs, MAX_TIMER_DELAY_MS);
|
|
3990
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
3991
|
+
void this.refreshTokenInBackground();
|
|
3992
|
+
}, delay);
|
|
3993
|
+
}
|
|
3994
|
+
async refreshTokenInBackground() {
|
|
3995
|
+
if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3998
|
+
try {
|
|
3999
|
+
const refreshedToken = await this.options.tokenProvider();
|
|
4000
|
+
if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
|
|
4001
|
+
throw new Error("Token provider returned no token");
|
|
4002
|
+
}
|
|
4003
|
+
this.token = refreshedToken;
|
|
4004
|
+
this.scheduleTokenRefresh();
|
|
4005
|
+
} catch (error) {
|
|
4006
|
+
if (this.isExplicitlyDisconnected) {
|
|
4007
|
+
return;
|
|
4008
|
+
}
|
|
4009
|
+
console.warn("[Granular] Token refresh failed, retrying soon:", error);
|
|
4010
|
+
this.clearTokenRefreshTimer();
|
|
4011
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
4012
|
+
void this.refreshTokenInBackground();
|
|
4013
|
+
}, TOKEN_REFRESH_RETRY_MS);
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
async resolveTokenForConnect() {
|
|
4017
|
+
if (!this.options.tokenProvider) {
|
|
4018
|
+
return this.token;
|
|
4019
|
+
}
|
|
4020
|
+
const expiresAt = this.getTokenExpiryMs(this.token);
|
|
4021
|
+
const shouldRefresh = expiresAt !== null && expiresAt - Date.now() <= TOKEN_REFRESH_LEEWAY_MS;
|
|
4022
|
+
if (!shouldRefresh) {
|
|
4023
|
+
return this.token;
|
|
4024
|
+
}
|
|
4025
|
+
try {
|
|
4026
|
+
const refreshedToken = await this.options.tokenProvider();
|
|
4027
|
+
if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
|
|
4028
|
+
throw new Error("Token provider returned no token");
|
|
4029
|
+
}
|
|
4030
|
+
this.token = refreshedToken;
|
|
4031
|
+
return refreshedToken;
|
|
4032
|
+
} catch (error) {
|
|
4033
|
+
if (expiresAt > Date.now()) {
|
|
4034
|
+
console.warn("[Granular] Token refresh failed, using current token:", error);
|
|
4035
|
+
return this.token;
|
|
4036
|
+
}
|
|
4037
|
+
throw error;
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
3941
4040
|
/**
|
|
3942
4041
|
* Connect to the WebSocket server
|
|
3943
4042
|
* @returns {Promise<void>} Resolves when connection is open
|
|
3944
4043
|
*/
|
|
3945
4044
|
async connect() {
|
|
4045
|
+
const token = await this.resolveTokenForConnect();
|
|
3946
4046
|
this.isExplicitlyDisconnected = false;
|
|
4047
|
+
this.scheduleTokenRefresh();
|
|
3947
4048
|
if (this.reconnectTimer) {
|
|
3948
4049
|
clearTimeout(this.reconnectTimer);
|
|
3949
4050
|
this.reconnectTimer = null;
|
|
@@ -3963,7 +4064,7 @@ var WSClient = class {
|
|
|
3963
4064
|
try {
|
|
3964
4065
|
const wsUrl = new URL(this.url);
|
|
3965
4066
|
wsUrl.searchParams.set("sessionId", this.sessionId);
|
|
3966
|
-
wsUrl.searchParams.set("token",
|
|
4067
|
+
wsUrl.searchParams.set("token", token);
|
|
3967
4068
|
this.ws = new WebSocketClass(wsUrl.toString());
|
|
3968
4069
|
if (!this.ws) throw new Error("Failed to create WebSocket");
|
|
3969
4070
|
const socket = this.ws;
|
|
@@ -4168,10 +4269,10 @@ var WSClient = class {
|
|
|
4168
4269
|
const snapshotMessage = message;
|
|
4169
4270
|
try {
|
|
4170
4271
|
const bytes = new Uint8Array(snapshotMessage.data);
|
|
4171
|
-
console.log("[Granular DEBUG] Loading snapshot bytes:", bytes.length);
|
|
4272
|
+
console.log("[Granular DEBUG] Loading Automerge session snapshot bytes:", bytes.length);
|
|
4172
4273
|
this.doc = Automerge.load(bytes);
|
|
4173
4274
|
this.emit("sync", this.doc);
|
|
4174
|
-
console.log("[Granular DEBUG]
|
|
4275
|
+
console.log("[Granular DEBUG] Automerge session snapshot loaded. Doc:", JSON.stringify(Automerge.toJS(this.doc)));
|
|
4175
4276
|
} catch (e) {
|
|
4176
4277
|
console.warn("[Granular] Failed to load snapshot message", e);
|
|
4177
4278
|
}
|
|
@@ -4334,6 +4435,7 @@ var WSClient = class {
|
|
|
4334
4435
|
clearTimeout(this.reconnectTimer);
|
|
4335
4436
|
this.reconnectTimer = null;
|
|
4336
4437
|
}
|
|
4438
|
+
this.clearTokenRefreshTimer();
|
|
4337
4439
|
if (this.ws) {
|
|
4338
4440
|
this.ws.close(1e3, "Client disconnect");
|
|
4339
4441
|
this.ws = null;
|
|
@@ -4364,6 +4466,17 @@ var Session = class {
|
|
|
4364
4466
|
this.setupEventHandlers();
|
|
4365
4467
|
this.setupToolInvokeHandler();
|
|
4366
4468
|
}
|
|
4469
|
+
buildLegacyEffectContext() {
|
|
4470
|
+
return {
|
|
4471
|
+
effectClientId: this.clientId,
|
|
4472
|
+
sandboxId: "",
|
|
4473
|
+
environmentId: "",
|
|
4474
|
+
sessionId: "",
|
|
4475
|
+
user: {
|
|
4476
|
+
subjectId: ""
|
|
4477
|
+
}
|
|
4478
|
+
};
|
|
4479
|
+
}
|
|
4367
4480
|
// --- Public API ---
|
|
4368
4481
|
get document() {
|
|
4369
4482
|
return this.client.doc;
|
|
@@ -4402,155 +4515,30 @@ var Session = class {
|
|
|
4402
4515
|
});
|
|
4403
4516
|
return result;
|
|
4404
4517
|
}
|
|
4405
|
-
/**
|
|
4406
|
-
* Publish tools to the sandbox and register handlers for reverse-RPC.
|
|
4407
|
-
*
|
|
4408
|
-
* Tools can be:
|
|
4409
|
-
* - **Instance methods**: set `className` (handler receives `(objectId, params)`)
|
|
4410
|
-
* - **Static methods**: set `className` + `static: true` (handler receives `(params)`)
|
|
4411
|
-
* - **Global tools**: omit `className` (handler receives `(params)`)
|
|
4412
|
-
*
|
|
4413
|
-
* Both `inputSchema` and `outputSchema` accept JSON Schema objects. The
|
|
4414
|
-
* `outputSchema` drives the return type in the auto-generated TypeScript
|
|
4415
|
-
* class declarations that sandbox code imports from `./sandbox-tools`.
|
|
4416
|
-
*
|
|
4417
|
-
* This method:
|
|
4418
|
-
* 1. Extracts tool schemas (including `outputSchema`) from the provided tools
|
|
4419
|
-
* 2. Publishes them via `client.publishRawToolCatalog` RPC
|
|
4420
|
-
* 3. Registers handlers locally for `tool.invoke` RPC calls
|
|
4421
|
-
* 4. Returns the `domainRevision` needed for job submission
|
|
4422
|
-
*
|
|
4423
|
-
* @param tools - Array of tools with handlers
|
|
4424
|
-
* @param revision - Optional revision string (default: "1.0.0")
|
|
4425
|
-
* @returns PublishToolsResult with domainRevision
|
|
4426
|
-
*
|
|
4427
|
-
* @example
|
|
4428
|
-
* ```typescript
|
|
4429
|
-
* await env.publishTools([
|
|
4430
|
-
* {
|
|
4431
|
-
* name: 'get_bio',
|
|
4432
|
-
* description: 'Get biography of an author',
|
|
4433
|
-
* className: 'author',
|
|
4434
|
-
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
4435
|
-
* outputSchema: { type: 'object', properties: { bio: { type: 'string' } }, required: ['bio'] },
|
|
4436
|
-
* handler: async (id, params) => ({ bio: `Bio of ${id}` }),
|
|
4437
|
-
* },
|
|
4438
|
-
* ]);
|
|
4439
|
-
* ```
|
|
4440
|
-
*/
|
|
4441
4518
|
async publishTools(tools, revision = "1.0.0") {
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
inputSchema: tool.inputSchema,
|
|
4446
|
-
outputSchema: tool.outputSchema,
|
|
4447
|
-
stability: tool.stability || "stable",
|
|
4448
|
-
provenance: tool.provenance || { source: "mcp" },
|
|
4449
|
-
tags: tool.tags,
|
|
4450
|
-
className: tool.className,
|
|
4451
|
-
static: tool.static
|
|
4452
|
-
}));
|
|
4453
|
-
const result = await this.client.call("client.publishRawToolCatalog", {
|
|
4454
|
-
clientId: this.clientId,
|
|
4455
|
-
revision,
|
|
4456
|
-
tools: schemas
|
|
4457
|
-
});
|
|
4458
|
-
if (!result.accepted || !result.domainRevision) {
|
|
4459
|
-
throw new Error(`Failed to publish tools: ${JSON.stringify(result.rejected)}`);
|
|
4460
|
-
}
|
|
4461
|
-
for (const tool of tools) {
|
|
4462
|
-
this.toolHandlers.set(tool.name, tool.handler);
|
|
4463
|
-
if (tool.className && !tool.static) {
|
|
4464
|
-
this.instanceTools.add(tool.name);
|
|
4465
|
-
} else {
|
|
4466
|
-
this.instanceTools.delete(tool.name);
|
|
4467
|
-
}
|
|
4468
|
-
}
|
|
4469
|
-
this.currentDomainRevision = result.domainRevision;
|
|
4470
|
-
return {
|
|
4471
|
-
accepted: result.accepted,
|
|
4472
|
-
domainRevision: result.domainRevision,
|
|
4473
|
-
rejected: result.rejected
|
|
4474
|
-
};
|
|
4519
|
+
throw new Error(
|
|
4520
|
+
"Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.registerEffects(environment.sandboxId, effects)."
|
|
4521
|
+
);
|
|
4475
4522
|
}
|
|
4476
|
-
/**
|
|
4477
|
-
* Publish a single effect (tool) to the sandbox.
|
|
4478
|
-
*
|
|
4479
|
-
* Adds the effect to the local registry and re-publishes the
|
|
4480
|
-
* full tool catalog to the server. If an effect with the same
|
|
4481
|
-
* name already exists, it is replaced.
|
|
4482
|
-
*
|
|
4483
|
-
* @param effect - The effect (tool) to publish
|
|
4484
|
-
* @returns PublishToolsResult with domainRevision
|
|
4485
|
-
*
|
|
4486
|
-
* @example
|
|
4487
|
-
* ```typescript
|
|
4488
|
-
* await env.publishEffect({
|
|
4489
|
-
* name: 'get_bio',
|
|
4490
|
-
* description: 'Get biography of an author',
|
|
4491
|
-
* className: 'author',
|
|
4492
|
-
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
4493
|
-
* handler: async (id, params) => ({ bio: `Bio of ${id}` }),
|
|
4494
|
-
* });
|
|
4495
|
-
* ```
|
|
4496
|
-
*/
|
|
4497
4523
|
async publishEffect(effect) {
|
|
4498
|
-
|
|
4499
|
-
|
|
4524
|
+
throw new Error(
|
|
4525
|
+
"Environment-scoped effect publication was removed. Use granular.registerEffect(environment.sandboxId, effect)."
|
|
4526
|
+
);
|
|
4500
4527
|
}
|
|
4501
|
-
/**
|
|
4502
|
-
* Publish multiple effects (tools) at once.
|
|
4503
|
-
*
|
|
4504
|
-
* Adds all effects to the local registry and re-publishes the
|
|
4505
|
-
* full tool catalog in a single RPC call.
|
|
4506
|
-
*
|
|
4507
|
-
* @param effects - Array of effects to publish
|
|
4508
|
-
* @returns PublishToolsResult with domainRevision
|
|
4509
|
-
*
|
|
4510
|
-
* @example
|
|
4511
|
-
* ```typescript
|
|
4512
|
-
* await env.publishEffects([
|
|
4513
|
-
* { name: 'get_bio', description: '...', inputSchema: {}, handler: async (id) => ({}) },
|
|
4514
|
-
* { name: 'search', description: '...', inputSchema: {}, handler: async (params) => ({}) },
|
|
4515
|
-
* ]);
|
|
4516
|
-
* ```
|
|
4517
|
-
*/
|
|
4518
4528
|
async publishEffects(effects) {
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
return this._syncEffects();
|
|
4529
|
+
throw new Error(
|
|
4530
|
+
"Environment-scoped effect publication was removed. Use granular.registerEffects(environment.sandboxId, effects)."
|
|
4531
|
+
);
|
|
4523
4532
|
}
|
|
4524
|
-
/**
|
|
4525
|
-
* Remove an effect by name and re-publish the remaining catalog.
|
|
4526
|
-
*
|
|
4527
|
-
* @param name - The name of the effect to remove
|
|
4528
|
-
* @returns PublishToolsResult with domainRevision
|
|
4529
|
-
*/
|
|
4530
4533
|
async unpublishEffect(name) {
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
return this._syncEffects();
|
|
4534
|
+
throw new Error(
|
|
4535
|
+
"Environment-scoped effect publication was removed. Use granular.unregisterEffect(environment.sandboxId, effectName)."
|
|
4536
|
+
);
|
|
4535
4537
|
}
|
|
4536
|
-
/**
|
|
4537
|
-
* Remove all effects and publish an empty catalog.
|
|
4538
|
-
*
|
|
4539
|
-
* @returns PublishToolsResult with domainRevision
|
|
4540
|
-
*/
|
|
4541
4538
|
async unpublishAllEffects() {
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
return this._syncEffects();
|
|
4546
|
-
}
|
|
4547
|
-
/**
|
|
4548
|
-
* Internal: re-publish the full effect catalog to the server.
|
|
4549
|
-
* Called after any mutation to the effects Map.
|
|
4550
|
-
*/
|
|
4551
|
-
async _syncEffects() {
|
|
4552
|
-
const allEffects = Array.from(this.effects.values());
|
|
4553
|
-
return this.publishTools(allEffects);
|
|
4539
|
+
throw new Error(
|
|
4540
|
+
"Environment-scoped effect publication was removed. Use granular.unregisterAllEffects(environment.sandboxId)."
|
|
4541
|
+
);
|
|
4554
4542
|
}
|
|
4555
4543
|
/**
|
|
4556
4544
|
* Submit a job to execute code in the sandbox.
|
|
@@ -4564,14 +4552,14 @@ var Session = class {
|
|
|
4564
4552
|
* const books = await tolkien.get_books();
|
|
4565
4553
|
* ```
|
|
4566
4554
|
*
|
|
4567
|
-
*
|
|
4568
|
-
* `
|
|
4555
|
+
* Effect calls (instance methods, static methods, global functions) trigger
|
|
4556
|
+
* `effect.invoke` RPC back to the sandbox effect host, where the registered handlers
|
|
4569
4557
|
* execute locally and return the result to the sandbox.
|
|
4570
4558
|
*/
|
|
4571
4559
|
async submitJob(code, domainRevision) {
|
|
4572
4560
|
const revision = domainRevision || this.currentDomainRevision || this.client.doc?.domain?.active || void 0;
|
|
4573
4561
|
if (!revision) {
|
|
4574
|
-
throw new Error("No domain revision available.
|
|
4562
|
+
throw new Error("No domain revision available. Register live effects or ensure the build schema is activated.");
|
|
4575
4563
|
}
|
|
4576
4564
|
const result = await this.client.call("job.submit", {
|
|
4577
4565
|
domainRevision: revision,
|
|
@@ -4603,10 +4591,10 @@ var Session = class {
|
|
|
4603
4591
|
await this.client.call("prompt.answer", { promptId, value: answer });
|
|
4604
4592
|
}
|
|
4605
4593
|
/**
|
|
4606
|
-
* Get the current list of available
|
|
4607
|
-
* Consolidates
|
|
4594
|
+
* Get the current list of available effects.
|
|
4595
|
+
* Consolidates effect declarations and live availability for the session.
|
|
4608
4596
|
*/
|
|
4609
|
-
|
|
4597
|
+
getEffects() {
|
|
4610
4598
|
const doc = this.client.doc;
|
|
4611
4599
|
const toolMap = /* @__PURE__ */ new Map();
|
|
4612
4600
|
const domainPkg = doc.domain?.packages?.domain;
|
|
@@ -4651,10 +4639,32 @@ var Session = class {
|
|
|
4651
4639
|
return Array.from(toolMap.values());
|
|
4652
4640
|
}
|
|
4653
4641
|
/**
|
|
4654
|
-
*
|
|
4642
|
+
* Backwards-compatible alias for `getEffects()`.
|
|
4643
|
+
*/
|
|
4644
|
+
getTools() {
|
|
4645
|
+
return this.getEffects();
|
|
4646
|
+
}
|
|
4647
|
+
/**
|
|
4648
|
+
* Subscribe to effect changes (added, removed, updated).
|
|
4655
4649
|
* @param callback - Function called with change events
|
|
4656
4650
|
* @returns Unsubscribe function
|
|
4657
4651
|
*/
|
|
4652
|
+
onEffectsChanged(callback) {
|
|
4653
|
+
const handler = (data) => callback(data);
|
|
4654
|
+
if (!this.eventListeners.has("effects:changed")) {
|
|
4655
|
+
this.eventListeners.set("effects:changed", []);
|
|
4656
|
+
}
|
|
4657
|
+
this.eventListeners.get("effects:changed").push(handler);
|
|
4658
|
+
return () => {
|
|
4659
|
+
const listeners = this.eventListeners.get("effects:changed");
|
|
4660
|
+
if (listeners) {
|
|
4661
|
+
this.eventListeners.set("effects:changed", listeners.filter((h) => h !== handler));
|
|
4662
|
+
}
|
|
4663
|
+
};
|
|
4664
|
+
}
|
|
4665
|
+
/**
|
|
4666
|
+
* Backwards-compatible alias for `onEffectsChanged()`.
|
|
4667
|
+
*/
|
|
4658
4668
|
onToolsChanged(callback) {
|
|
4659
4669
|
const handler = (data) => callback(data);
|
|
4660
4670
|
if (!this.eventListeners.has("tools:changed")) {
|
|
@@ -4767,7 +4777,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4767
4777
|
}
|
|
4768
4778
|
}
|
|
4769
4779
|
if (globalTools && globalTools.length > 0) {
|
|
4770
|
-
docs2 += "## Global
|
|
4780
|
+
docs2 += "## Global Effects\n\n";
|
|
4771
4781
|
for (const tool of globalTools) {
|
|
4772
4782
|
docs2 += `### ${tool.name}
|
|
4773
4783
|
|
|
@@ -4785,10 +4795,10 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4785
4795
|
return docs2;
|
|
4786
4796
|
}
|
|
4787
4797
|
if (!tools || tools.length === 0) {
|
|
4788
|
-
return "No
|
|
4798
|
+
return "No effects available in this domain.";
|
|
4789
4799
|
}
|
|
4790
|
-
let docs = "# Available
|
|
4791
|
-
docs += "Import
|
|
4800
|
+
let docs = "# Available Effects\n\n";
|
|
4801
|
+
docs += "Import effects from `./sandbox-tools` and call them with await:\n\n";
|
|
4792
4802
|
docs += '```typescript\nimport { tools } from "./sandbox-tools";\n\n';
|
|
4793
4803
|
docs += "// Example:\n";
|
|
4794
4804
|
docs += `const result = await tools.${tools[0]?.name || "example"}(input);
|
|
@@ -4820,6 +4830,13 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4820
4830
|
* Close the session and disconnect from the sandbox
|
|
4821
4831
|
*/
|
|
4822
4832
|
async disconnect() {
|
|
4833
|
+
try {
|
|
4834
|
+
await this.client.call("client.goodbye", {
|
|
4835
|
+
clientId: this.clientId,
|
|
4836
|
+
timestamp: Date.now()
|
|
4837
|
+
});
|
|
4838
|
+
} catch (error) {
|
|
4839
|
+
}
|
|
4823
4840
|
this.client.disconnect();
|
|
4824
4841
|
}
|
|
4825
4842
|
// --- Event Handling ---
|
|
@@ -4845,6 +4862,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4845
4862
|
setupToolInvokeHandler() {
|
|
4846
4863
|
this.client.registerRpcHandler("tool.invoke", async (params) => {
|
|
4847
4864
|
const { callId, toolName, input } = params;
|
|
4865
|
+
this.emit("effect:invoke", { callId, effectKey: toolName, toolName, input });
|
|
4848
4866
|
this.emit("tool:invoke", { callId, toolName, input });
|
|
4849
4867
|
const handler = this.toolHandlers.get(toolName);
|
|
4850
4868
|
if (!handler) {
|
|
@@ -4856,12 +4874,14 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4856
4874
|
}
|
|
4857
4875
|
try {
|
|
4858
4876
|
let result;
|
|
4877
|
+
const invocationContext = this.buildLegacyEffectContext();
|
|
4859
4878
|
if (this.instanceTools.has(toolName) && input && typeof input === "object" && "_objectId" in input) {
|
|
4860
4879
|
const { _objectId, ...restParams } = input;
|
|
4861
|
-
result = await handler(_objectId, restParams);
|
|
4880
|
+
result = await handler(_objectId, restParams, invocationContext);
|
|
4862
4881
|
} else {
|
|
4863
|
-
result = await handler(input);
|
|
4882
|
+
result = await handler(input, invocationContext);
|
|
4864
4883
|
}
|
|
4884
|
+
this.emit("effect:result", { callId, effectKey: toolName, result });
|
|
4865
4885
|
this.emit("tool:result", { callId, result });
|
|
4866
4886
|
await this.client.call("tool.result", {
|
|
4867
4887
|
callId,
|
|
@@ -4869,6 +4889,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4869
4889
|
});
|
|
4870
4890
|
} catch (error) {
|
|
4871
4891
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4892
|
+
this.emit("effect:result", { callId, effectKey: toolName, error: errorMessage });
|
|
4872
4893
|
this.emit("tool:result", { callId, error: errorMessage });
|
|
4873
4894
|
await this.client.call("tool.result", {
|
|
4874
4895
|
callId,
|
|
@@ -4901,10 +4922,10 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4901
4922
|
}
|
|
4902
4923
|
}
|
|
4903
4924
|
/**
|
|
4904
|
-
* Check for changes in the
|
|
4925
|
+
* Check for changes in the effect catalog and emit change events if needed.
|
|
4905
4926
|
*/
|
|
4906
4927
|
checkForToolChanges() {
|
|
4907
|
-
const currentTools = this.
|
|
4928
|
+
const currentTools = this.getEffects();
|
|
4908
4929
|
const currentMap = new Map(currentTools.map((t) => [t.name, t]));
|
|
4909
4930
|
const added = [];
|
|
4910
4931
|
const removed = [];
|
|
@@ -4920,6 +4941,12 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4920
4941
|
}
|
|
4921
4942
|
if (added.length > 0 || removed.length > 0) {
|
|
4922
4943
|
this.lastKnownTools = currentMap;
|
|
4944
|
+
this.emit("effects:changed", {
|
|
4945
|
+
effects: currentTools,
|
|
4946
|
+
tools: currentTools,
|
|
4947
|
+
added,
|
|
4948
|
+
removed
|
|
4949
|
+
});
|
|
4923
4950
|
this.emit("tools:changed", {
|
|
4924
4951
|
tools: currentTools,
|
|
4925
4952
|
added,
|
|
@@ -5030,6 +5057,50 @@ var JobImplementation = class {
|
|
|
5030
5057
|
}
|
|
5031
5058
|
};
|
|
5032
5059
|
|
|
5060
|
+
// src/endpoints.ts
|
|
5061
|
+
var LOCAL_API_URL = "ws://localhost:8787/granular";
|
|
5062
|
+
var PRODUCTION_API_URL = "wss://api.granular.dev/v2/ws";
|
|
5063
|
+
function readEnv(name) {
|
|
5064
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
5065
|
+
return process.env[name];
|
|
5066
|
+
}
|
|
5067
|
+
function normalizeMode(value) {
|
|
5068
|
+
if (!value) return void 0;
|
|
5069
|
+
const normalized = value.trim().toLowerCase();
|
|
5070
|
+
if (normalized === "local") return "local";
|
|
5071
|
+
if (normalized === "prod" || normalized === "production") return "production";
|
|
5072
|
+
if (normalized === "auto") return "auto";
|
|
5073
|
+
return void 0;
|
|
5074
|
+
}
|
|
5075
|
+
function isTruthy(value) {
|
|
5076
|
+
if (!value) return false;
|
|
5077
|
+
const normalized = value.trim().toLowerCase();
|
|
5078
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
5079
|
+
}
|
|
5080
|
+
function resolveEndpointMode(explicitMode) {
|
|
5081
|
+
const explicit = normalizeMode(explicitMode);
|
|
5082
|
+
if (explicit === "local" || explicit === "production") {
|
|
5083
|
+
return explicit;
|
|
5084
|
+
}
|
|
5085
|
+
const envMode = normalizeMode(readEnv("GRANULAR_ENDPOINT_MODE") || readEnv("GRANULAR_ENV"));
|
|
5086
|
+
if (envMode === "local" || envMode === "production") {
|
|
5087
|
+
return envMode;
|
|
5088
|
+
}
|
|
5089
|
+
if (isTruthy(readEnv("GRANULAR_USE_LOCAL_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_LOCAL"))) {
|
|
5090
|
+
return "local";
|
|
5091
|
+
}
|
|
5092
|
+
if (isTruthy(readEnv("GRANULAR_USE_PRODUCTION_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_PROD"))) {
|
|
5093
|
+
return "production";
|
|
5094
|
+
}
|
|
5095
|
+
return readEnv("NODE_ENV") === "development" ? "local" : "production";
|
|
5096
|
+
}
|
|
5097
|
+
function resolveApiUrl(explicitApiUrl, mode) {
|
|
5098
|
+
if (explicitApiUrl) {
|
|
5099
|
+
return explicitApiUrl;
|
|
5100
|
+
}
|
|
5101
|
+
return resolveEndpointMode(mode) === "local" ? LOCAL_API_URL : PRODUCTION_API_URL;
|
|
5102
|
+
}
|
|
5103
|
+
|
|
5033
5104
|
// src/client.ts
|
|
5034
5105
|
var STANDARD_MODULES_OPERATIONS = [
|
|
5035
5106
|
{ create: "entity", has: { id: { value: "auto-generated" }, createdAt: { value: void 0 } } },
|
|
@@ -5044,6 +5115,37 @@ var STANDARD_MODULES_OPERATIONS = [
|
|
|
5044
5115
|
var BUILTIN_MODULES = {
|
|
5045
5116
|
"standard_modules": STANDARD_MODULES_OPERATIONS
|
|
5046
5117
|
};
|
|
5118
|
+
function computeEffectKey(effect) {
|
|
5119
|
+
const attachedClass = effect.className?.trim();
|
|
5120
|
+
if (!attachedClass) {
|
|
5121
|
+
return `global:${effect.name}`;
|
|
5122
|
+
}
|
|
5123
|
+
return effect.static ? `class:${attachedClass}:static:${effect.name}` : `class:${attachedClass}:instance:${effect.name}`;
|
|
5124
|
+
}
|
|
5125
|
+
function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId) {
|
|
5126
|
+
const url = new URL(apiUrl);
|
|
5127
|
+
if (url.pathname.endsWith("/granular/ws/connect")) {
|
|
5128
|
+
url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
|
|
5129
|
+
} else if (url.pathname.endsWith("/granular")) {
|
|
5130
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/effects/connect`;
|
|
5131
|
+
} else if (url.pathname.endsWith("/v2/ws/connect")) {
|
|
5132
|
+
url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
|
|
5133
|
+
} else if (url.pathname.endsWith("/v2/ws")) {
|
|
5134
|
+
url.pathname = url.pathname.replace(/\/ws$/, "/effects/connect");
|
|
5135
|
+
} else if (url.pathname.endsWith("/ws/connect")) {
|
|
5136
|
+
url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
|
|
5137
|
+
} else if (url.pathname.endsWith("/ws")) {
|
|
5138
|
+
url.pathname = url.pathname.replace(/\/ws$/, "/effects/connect");
|
|
5139
|
+
} else {
|
|
5140
|
+
url.pathname = "/granular/effects/connect";
|
|
5141
|
+
}
|
|
5142
|
+
url.search = "";
|
|
5143
|
+
url.hash = "";
|
|
5144
|
+
url.searchParams.set("sandboxId", sandboxId);
|
|
5145
|
+
url.searchParams.set("effectClientId", effectClientId);
|
|
5146
|
+
url.searchParams.set("clientId", clientId);
|
|
5147
|
+
return url.toString();
|
|
5148
|
+
}
|
|
5047
5149
|
var Environment = class _Environment extends Session {
|
|
5048
5150
|
envData;
|
|
5049
5151
|
_apiKey;
|
|
@@ -5074,6 +5176,60 @@ var Environment = class _Environment extends Session {
|
|
|
5074
5176
|
get apiEndpoint() {
|
|
5075
5177
|
return this._apiEndpoint;
|
|
5076
5178
|
}
|
|
5179
|
+
getRuntimeBaseUrl() {
|
|
5180
|
+
try {
|
|
5181
|
+
const endpoint = new URL(this._apiEndpoint);
|
|
5182
|
+
const graphqlSuffix = "/orchestrator/graphql";
|
|
5183
|
+
if (endpoint.pathname.endsWith(graphqlSuffix)) {
|
|
5184
|
+
endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
|
|
5185
|
+
} else if (endpoint.pathname.endsWith("/graphql")) {
|
|
5186
|
+
endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
|
|
5187
|
+
}
|
|
5188
|
+
endpoint.search = "";
|
|
5189
|
+
endpoint.hash = "";
|
|
5190
|
+
return endpoint.toString().replace(/\/$/, "");
|
|
5191
|
+
} catch {
|
|
5192
|
+
return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
/**
|
|
5196
|
+
* Close the session and disconnect from the sandbox.
|
|
5197
|
+
*
|
|
5198
|
+
* Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
|
|
5199
|
+
* to the runtime goodbye endpoint if no definitive WS-side runtime notify
|
|
5200
|
+
* acknowledgement was observed.
|
|
5201
|
+
*/
|
|
5202
|
+
async disconnect() {
|
|
5203
|
+
let wsNotifiedRuntime = false;
|
|
5204
|
+
try {
|
|
5205
|
+
const goodbye = await this.rpc("client.goodbye", {
|
|
5206
|
+
timestamp: Date.now()
|
|
5207
|
+
});
|
|
5208
|
+
wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
|
|
5209
|
+
} catch {
|
|
5210
|
+
wsNotifiedRuntime = false;
|
|
5211
|
+
}
|
|
5212
|
+
if (!wsNotifiedRuntime) {
|
|
5213
|
+
try {
|
|
5214
|
+
const runtimeBase = this.getRuntimeBaseUrl();
|
|
5215
|
+
await fetch(
|
|
5216
|
+
`${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
|
|
5217
|
+
{
|
|
5218
|
+
method: "POST",
|
|
5219
|
+
headers: {
|
|
5220
|
+
"Content-Type": "application/json",
|
|
5221
|
+
"Authorization": `Bearer ${this._apiKey}`
|
|
5222
|
+
},
|
|
5223
|
+
body: JSON.stringify({
|
|
5224
|
+
reason: "sdk_disconnect_http_fallback"
|
|
5225
|
+
})
|
|
5226
|
+
}
|
|
5227
|
+
);
|
|
5228
|
+
} catch {
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
this.client.disconnect();
|
|
5232
|
+
}
|
|
5077
5233
|
// ==================== GRAPH CONTAINER READINESS ====================
|
|
5078
5234
|
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
5079
5235
|
graphContainerStatus = null;
|
|
@@ -5734,72 +5890,31 @@ var Environment = class _Environment extends Session {
|
|
|
5734
5890
|
}
|
|
5735
5891
|
// ==================== PUBLISH TOOLS ====================
|
|
5736
5892
|
/**
|
|
5737
|
-
*
|
|
5738
|
-
* This is the main entry point for setting up tools.
|
|
5739
|
-
*
|
|
5740
|
-
* @example
|
|
5741
|
-
* ```typescript
|
|
5742
|
-
* const tools = [
|
|
5743
|
-
* {
|
|
5744
|
-
* name: 'get_weather',
|
|
5745
|
-
* description: 'Get weather for a city',
|
|
5746
|
-
* inputSchema: { type: 'object', properties: { city: { type: 'string' } } },
|
|
5747
|
-
* handler: async ({ city }) => ({ temp: 22 }),
|
|
5748
|
-
* },
|
|
5749
|
-
* ];
|
|
5750
|
-
*
|
|
5751
|
-
* const { domainRevision } = await environment.publishTools(tools);
|
|
5752
|
-
*
|
|
5753
|
-
* // Now submit jobs that use those tools
|
|
5754
|
-
* const job = await environment.submitJob(`
|
|
5755
|
-
* import { Author } from './sandbox-tools';
|
|
5756
|
-
* const weather = await Author.get_weather({ city: 'Paris' });
|
|
5757
|
-
* return weather;
|
|
5758
|
-
* `);
|
|
5759
|
-
*
|
|
5760
|
-
* const result = await job.result;
|
|
5761
|
-
* ```
|
|
5893
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5762
5894
|
*/
|
|
5763
5895
|
async publishTools(tools, revision = "1.0.0") {
|
|
5764
5896
|
return super.publishTools(tools, revision);
|
|
5765
5897
|
}
|
|
5766
5898
|
/**
|
|
5767
|
-
*
|
|
5768
|
-
*
|
|
5769
|
-
* Adds the effect to the local registry and re-publishes the
|
|
5770
|
-
* full tool catalog to the server. If an effect with the same
|
|
5771
|
-
* name already exists, it is replaced.
|
|
5772
|
-
*
|
|
5773
|
-
* @example
|
|
5774
|
-
* ```typescript
|
|
5775
|
-
* await env.publishEffect({
|
|
5776
|
-
* name: 'get_bio',
|
|
5777
|
-
* description: 'Get biography',
|
|
5778
|
-
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
5779
|
-
* handler: async (params) => ({ bio: 'Hello' }),
|
|
5780
|
-
* });
|
|
5781
|
-
* ```
|
|
5899
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5782
5900
|
*/
|
|
5783
5901
|
async publishEffect(effect) {
|
|
5784
5902
|
return super.publishEffect(effect);
|
|
5785
5903
|
}
|
|
5786
5904
|
/**
|
|
5787
|
-
*
|
|
5788
|
-
*
|
|
5789
|
-
* Adds all effects to the local registry and re-publishes the
|
|
5790
|
-
* full tool catalog in a single RPC call.
|
|
5905
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5791
5906
|
*/
|
|
5792
5907
|
async publishEffects(effects) {
|
|
5793
5908
|
return super.publishEffects(effects);
|
|
5794
5909
|
}
|
|
5795
5910
|
/**
|
|
5796
|
-
*
|
|
5911
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5797
5912
|
*/
|
|
5798
5913
|
async unpublishEffect(name) {
|
|
5799
5914
|
return super.unpublishEffect(name);
|
|
5800
5915
|
}
|
|
5801
5916
|
/**
|
|
5802
|
-
*
|
|
5917
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5803
5918
|
*/
|
|
5804
5919
|
async unpublishAllEffects() {
|
|
5805
5920
|
return super.unpublishAllEffects();
|
|
@@ -5809,13 +5924,16 @@ var Granular = class {
|
|
|
5809
5924
|
apiKey;
|
|
5810
5925
|
apiUrl;
|
|
5811
5926
|
httpUrl;
|
|
5927
|
+
tokenProvider;
|
|
5812
5928
|
WebSocketCtor;
|
|
5813
5929
|
onUnexpectedClose;
|
|
5814
5930
|
onReconnectError;
|
|
5815
|
-
/** Sandbox-level effect registry: sandboxId → (
|
|
5931
|
+
/** Sandbox-level effect registry: sandboxId → (effectKey → ToolWithHandler) */
|
|
5816
5932
|
sandboxEffects = /* @__PURE__ */ new Map();
|
|
5817
|
-
/**
|
|
5818
|
-
|
|
5933
|
+
/** Live sandbox-scoped effect hosts keyed by sandboxId */
|
|
5934
|
+
sandboxEffectHosts = /* @__PURE__ */ new Map();
|
|
5935
|
+
/** In-flight host connection promises to avoid duplicate concurrent connects */
|
|
5936
|
+
sandboxEffectHostPromises = /* @__PURE__ */ new Map();
|
|
5819
5937
|
/**
|
|
5820
5938
|
* Create a new Granular client
|
|
5821
5939
|
* @param options - Client configuration
|
|
@@ -5826,11 +5944,12 @@ var Granular = class {
|
|
|
5826
5944
|
throw new Error("Granular client requires either apiKey or token. Set GRANULAR_API_KEY or GRANULAR_TOKEN, or pass one in options.");
|
|
5827
5945
|
}
|
|
5828
5946
|
this.apiKey = auth;
|
|
5829
|
-
this.apiUrl = options.apiUrl
|
|
5947
|
+
this.apiUrl = resolveApiUrl(options.apiUrl, options.endpointMode);
|
|
5948
|
+
this.tokenProvider = options.tokenProvider;
|
|
5830
5949
|
this.WebSocketCtor = options.WebSocketCtor;
|
|
5831
5950
|
this.onUnexpectedClose = options.onUnexpectedClose;
|
|
5832
5951
|
this.onReconnectError = options.onReconnectError;
|
|
5833
|
-
this.httpUrl = this.apiUrl.replace("
|
|
5952
|
+
this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
|
|
5834
5953
|
}
|
|
5835
5954
|
/**
|
|
5836
5955
|
* Records/upserts a user and prepares them for sandbox connections
|
|
@@ -5867,8 +5986,9 @@ var Granular = class {
|
|
|
5867
5986
|
/**
|
|
5868
5987
|
* Connect to a sandbox and establish a real-time environment session.
|
|
5869
5988
|
*
|
|
5870
|
-
*
|
|
5871
|
-
*
|
|
5989
|
+
* Effects are registered at the sandbox level via `granular.registerEffect()`
|
|
5990
|
+
* or `granular.registerEffects()`. Sessions pick up live availability from
|
|
5991
|
+
* the sandbox registry automatically.
|
|
5872
5992
|
*
|
|
5873
5993
|
* @param options - Connection options
|
|
5874
5994
|
* @returns An active environment session
|
|
@@ -5885,10 +6005,12 @@ var Granular = class {
|
|
|
5885
6005
|
* user,
|
|
5886
6006
|
* });
|
|
5887
6007
|
*
|
|
5888
|
-
*
|
|
5889
|
-
*
|
|
5890
|
-
*
|
|
5891
|
-
*
|
|
6008
|
+
* await granular.registerEffect('my-sandbox', {
|
|
6009
|
+
* name: 'greet',
|
|
6010
|
+
* description: 'Say hello',
|
|
6011
|
+
* inputSchema: { type: 'object', properties: {} },
|
|
6012
|
+
* handler: async () => 'Hello!',
|
|
6013
|
+
* });
|
|
5892
6014
|
*
|
|
5893
6015
|
* // Submit job
|
|
5894
6016
|
* const job = await environment.submitJob(`
|
|
@@ -5910,10 +6032,19 @@ var Granular = class {
|
|
|
5910
6032
|
subjectId: options.user.subjectId,
|
|
5911
6033
|
permissionProfileId: null
|
|
5912
6034
|
});
|
|
6035
|
+
await this.activateEnvironment(envData.environmentId);
|
|
6036
|
+
const session = await this.request("/ws/sessions", {
|
|
6037
|
+
method: "POST",
|
|
6038
|
+
body: JSON.stringify({
|
|
6039
|
+
environmentId: envData.environmentId,
|
|
6040
|
+
clientId
|
|
6041
|
+
})
|
|
6042
|
+
});
|
|
5913
6043
|
const client = new WSClient({
|
|
5914
|
-
url:
|
|
5915
|
-
sessionId:
|
|
5916
|
-
token:
|
|
6044
|
+
url: session.wsUrl,
|
|
6045
|
+
sessionId: session.sessionId,
|
|
6046
|
+
token: session.token,
|
|
6047
|
+
tokenProvider: this.tokenProvider,
|
|
5917
6048
|
WebSocketCtor: this.WebSocketCtor,
|
|
5918
6049
|
onUnexpectedClose: this.onUnexpectedClose,
|
|
5919
6050
|
onReconnectError: this.onReconnectError
|
|
@@ -5921,47 +6052,165 @@ var Granular = class {
|
|
|
5921
6052
|
await client.connect();
|
|
5922
6053
|
const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
|
|
5923
6054
|
const environment = new Environment(client, envData, clientId, this.apiKey, graphqlEndpoint);
|
|
5924
|
-
if (!this.activeEnvironments.has(sandbox.sandboxId)) {
|
|
5925
|
-
this.activeEnvironments.set(sandbox.sandboxId, []);
|
|
5926
|
-
}
|
|
5927
|
-
this.activeEnvironments.get(sandbox.sandboxId).push(environment);
|
|
5928
|
-
environment.on("disconnect", () => {
|
|
5929
|
-
const list = this.activeEnvironments.get(sandbox.sandboxId);
|
|
5930
|
-
if (list) {
|
|
5931
|
-
this.activeEnvironments.set(sandbox.sandboxId, list.filter((e) => e !== environment));
|
|
5932
|
-
}
|
|
5933
|
-
});
|
|
5934
6055
|
await environment.hello();
|
|
5935
|
-
const effects = this.sandboxEffects.get(sandbox.sandboxId);
|
|
5936
|
-
if (effects && effects.size > 0) {
|
|
5937
|
-
const effectsList = Array.from(effects.values());
|
|
5938
|
-
console.log(`[Granular] Auto-publishing ${effectsList.length} effects for sandbox ${sandbox.sandboxId}`);
|
|
5939
|
-
await environment.publishEffects(effectsList);
|
|
5940
|
-
}
|
|
5941
6056
|
return environment;
|
|
5942
6057
|
}
|
|
6058
|
+
async activateEnvironment(environmentId) {
|
|
6059
|
+
await this.request(`/orchestrator/runtime/environments/${environmentId}/activate`, {
|
|
6060
|
+
method: "POST",
|
|
6061
|
+
body: JSON.stringify({})
|
|
6062
|
+
});
|
|
6063
|
+
}
|
|
5943
6064
|
// ── Sandbox-Level Effects ──
|
|
6065
|
+
getSandboxEffectMap(sandboxId) {
|
|
6066
|
+
let effects = this.sandboxEffects.get(sandboxId);
|
|
6067
|
+
if (!effects) {
|
|
6068
|
+
effects = /* @__PURE__ */ new Map();
|
|
6069
|
+
this.sandboxEffects.set(sandboxId, effects);
|
|
6070
|
+
}
|
|
6071
|
+
return effects;
|
|
6072
|
+
}
|
|
6073
|
+
serializeEffect(effect) {
|
|
6074
|
+
return {
|
|
6075
|
+
effectKey: computeEffectKey(effect),
|
|
6076
|
+
name: effect.name,
|
|
6077
|
+
description: effect.description,
|
|
6078
|
+
inputSchema: effect.inputSchema,
|
|
6079
|
+
outputSchema: effect.outputSchema,
|
|
6080
|
+
stability: effect.stability || "stable",
|
|
6081
|
+
provenance: effect.provenance || { source: "custom" },
|
|
6082
|
+
tags: effect.tags,
|
|
6083
|
+
className: effect.className,
|
|
6084
|
+
static: effect.static
|
|
6085
|
+
};
|
|
6086
|
+
}
|
|
6087
|
+
async publishSandboxEffectCatalog(host) {
|
|
6088
|
+
const effects = Array.from(this.getSandboxEffectMap(host.sandboxId).values()).map(
|
|
6089
|
+
(effect) => this.serializeEffect(effect)
|
|
6090
|
+
);
|
|
6091
|
+
await host.wsClient.call("effects.publishCatalog", { effects });
|
|
6092
|
+
}
|
|
6093
|
+
async syncSandboxEffectCatalog(sandboxId) {
|
|
6094
|
+
const host = await this.ensureSandboxEffectHost(sandboxId);
|
|
6095
|
+
await this.publishSandboxEffectCatalog(host);
|
|
6096
|
+
}
|
|
6097
|
+
startEffectHostHeartbeat(host) {
|
|
6098
|
+
if (host.heartbeatTimer) {
|
|
6099
|
+
clearInterval(host.heartbeatTimer);
|
|
6100
|
+
}
|
|
6101
|
+
host.wsClient.call("client.heartbeat", {}).catch((error) => {
|
|
6102
|
+
console.warn(
|
|
6103
|
+
`[Granular] Initial effect host heartbeat failed for sandbox ${host.sandboxId}:`,
|
|
6104
|
+
error
|
|
6105
|
+
);
|
|
6106
|
+
});
|
|
6107
|
+
host.heartbeatTimer = setInterval(() => {
|
|
6108
|
+
host.wsClient.call("client.heartbeat", {}).catch((error) => {
|
|
6109
|
+
console.warn(
|
|
6110
|
+
`[Granular] Effect host heartbeat failed for sandbox ${host.sandboxId}:`,
|
|
6111
|
+
error
|
|
6112
|
+
);
|
|
6113
|
+
});
|
|
6114
|
+
}, 1e4);
|
|
6115
|
+
}
|
|
6116
|
+
stopEffectHostHeartbeat(host) {
|
|
6117
|
+
if (!host?.heartbeatTimer) {
|
|
6118
|
+
return;
|
|
6119
|
+
}
|
|
6120
|
+
clearInterval(host.heartbeatTimer);
|
|
6121
|
+
host.heartbeatTimer = null;
|
|
6122
|
+
}
|
|
6123
|
+
async synchronizeEffectHost(host) {
|
|
6124
|
+
await host.wsClient.call("client.hello", {
|
|
6125
|
+
clientId: host.clientId,
|
|
6126
|
+
protocolVersion: "2.0"
|
|
6127
|
+
});
|
|
6128
|
+
this.startEffectHostHeartbeat(host);
|
|
6129
|
+
await this.publishSandboxEffectCatalog(host);
|
|
6130
|
+
}
|
|
6131
|
+
async ensureSandboxEffectHost(sandboxId) {
|
|
6132
|
+
const existing = this.sandboxEffectHosts.get(sandboxId);
|
|
6133
|
+
if (existing) {
|
|
6134
|
+
return existing;
|
|
6135
|
+
}
|
|
6136
|
+
const inflight = this.sandboxEffectHostPromises.get(sandboxId);
|
|
6137
|
+
if (inflight) {
|
|
6138
|
+
return inflight;
|
|
6139
|
+
}
|
|
6140
|
+
const connectPromise = (async () => {
|
|
6141
|
+
const effectClientId = crypto.randomUUID();
|
|
6142
|
+
const clientId = `effect-host:${sandboxId}:${effectClientId}`;
|
|
6143
|
+
const wsClient = new WSClient({
|
|
6144
|
+
url: buildEffectHostUrl(this.apiUrl, sandboxId, effectClientId, clientId),
|
|
6145
|
+
sessionId: `effect-host:${effectClientId}`,
|
|
6146
|
+
token: this.apiKey,
|
|
6147
|
+
tokenProvider: this.tokenProvider,
|
|
6148
|
+
WebSocketCtor: this.WebSocketCtor,
|
|
6149
|
+
onUnexpectedClose: this.onUnexpectedClose,
|
|
6150
|
+
onReconnectError: this.onReconnectError
|
|
6151
|
+
});
|
|
6152
|
+
const host = {
|
|
6153
|
+
sandboxId,
|
|
6154
|
+
effectClientId,
|
|
6155
|
+
clientId,
|
|
6156
|
+
wsClient,
|
|
6157
|
+
heartbeatTimer: null
|
|
6158
|
+
};
|
|
6159
|
+
wsClient.registerRpcHandler("effect.invoke", async (params) => {
|
|
6160
|
+
const request = params;
|
|
6161
|
+
const effect = this.getSandboxEffectMap(sandboxId).get(request.effectKey);
|
|
6162
|
+
if (!effect) {
|
|
6163
|
+
throw new Error(`Effect handler not found: ${request.effectKey}`);
|
|
6164
|
+
}
|
|
6165
|
+
if (effect.className && !effect.static && request.input && typeof request.input === "object" && "_objectId" in request.input) {
|
|
6166
|
+
const { _objectId, ...rest } = request.input;
|
|
6167
|
+
return effect.handler(_objectId, rest, request.context);
|
|
6168
|
+
}
|
|
6169
|
+
return effect.handler(request.input, request.context);
|
|
6170
|
+
});
|
|
6171
|
+
wsClient.on("open", () => {
|
|
6172
|
+
void this.synchronizeEffectHost(host).catch((error) => {
|
|
6173
|
+
console.error(
|
|
6174
|
+
`[Granular] Failed to re-sync effect host for sandbox ${sandboxId}:`,
|
|
6175
|
+
error
|
|
6176
|
+
);
|
|
6177
|
+
});
|
|
6178
|
+
});
|
|
6179
|
+
wsClient.on("disconnect", () => {
|
|
6180
|
+
this.stopEffectHostHeartbeat(host);
|
|
6181
|
+
});
|
|
6182
|
+
await wsClient.connect();
|
|
6183
|
+
await this.synchronizeEffectHost(host);
|
|
6184
|
+
this.sandboxEffectHosts.set(sandboxId, host);
|
|
6185
|
+
return host;
|
|
6186
|
+
})();
|
|
6187
|
+
this.sandboxEffectHostPromises.set(sandboxId, connectPromise);
|
|
6188
|
+
try {
|
|
6189
|
+
return await connectPromise;
|
|
6190
|
+
} finally {
|
|
6191
|
+
this.sandboxEffectHostPromises.delete(sandboxId);
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
disconnectSandboxEffectHost(sandboxId) {
|
|
6195
|
+
const host = this.sandboxEffectHosts.get(sandboxId);
|
|
6196
|
+
if (!host) {
|
|
6197
|
+
return;
|
|
6198
|
+
}
|
|
6199
|
+
this.stopEffectHostHeartbeat(host);
|
|
6200
|
+
host.wsClient.disconnect();
|
|
6201
|
+
this.sandboxEffectHosts.delete(sandboxId);
|
|
6202
|
+
}
|
|
5944
6203
|
/**
|
|
5945
6204
|
* Register an effect (tool) for a specific sandbox.
|
|
5946
|
-
*
|
|
5947
|
-
* The effect will be automatically published to:
|
|
5948
|
-
* 1. Any currently active environments for this sandbox
|
|
5949
|
-
* 2. Any new environments created/connected for this sandbox
|
|
5950
|
-
*
|
|
6205
|
+
*
|
|
5951
6206
|
* @param sandboxNameOrId - The name or ID of the sandbox
|
|
5952
6207
|
* @param effect - The tool definition and handler
|
|
5953
6208
|
*/
|
|
5954
6209
|
async registerEffect(sandboxNameOrId, effect) {
|
|
5955
6210
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
5956
6211
|
const sandboxId = sandbox.sandboxId;
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
}
|
|
5960
|
-
this.sandboxEffects.get(sandboxId).set(effect.name, effect);
|
|
5961
|
-
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
5962
|
-
for (const env of activeEnvs) {
|
|
5963
|
-
await env.publishEffect(effect);
|
|
5964
|
-
}
|
|
6212
|
+
this.getSandboxEffectMap(sandboxId).set(computeEffectKey(effect), effect);
|
|
6213
|
+
await this.syncSandboxEffectCatalog(sandboxId);
|
|
5965
6214
|
}
|
|
5966
6215
|
/**
|
|
5967
6216
|
* Register multiple effects (tools) for a specific sandbox.
|
|
@@ -5971,35 +6220,39 @@ var Granular = class {
|
|
|
5971
6220
|
async registerEffects(sandboxNameOrId, effects) {
|
|
5972
6221
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
5973
6222
|
const sandboxId = sandbox.sandboxId;
|
|
5974
|
-
|
|
5975
|
-
this.sandboxEffects.set(sandboxId, /* @__PURE__ */ new Map());
|
|
5976
|
-
}
|
|
5977
|
-
const map = this.sandboxEffects.get(sandboxId);
|
|
6223
|
+
const map = this.getSandboxEffectMap(sandboxId);
|
|
5978
6224
|
for (const effect of effects) {
|
|
5979
|
-
map.set(effect
|
|
5980
|
-
}
|
|
5981
|
-
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
5982
|
-
for (const env of activeEnvs) {
|
|
5983
|
-
await env.publishEffects(effects);
|
|
6225
|
+
map.set(computeEffectKey(effect), effect);
|
|
5984
6226
|
}
|
|
6227
|
+
await this.syncSandboxEffectCatalog(sandboxId);
|
|
5985
6228
|
}
|
|
5986
6229
|
/**
|
|
5987
6230
|
* Unregister an effect from a sandbox.
|
|
5988
6231
|
*
|
|
5989
|
-
* Removes it from the local registry and
|
|
5990
|
-
*
|
|
6232
|
+
* Removes it from the local sandbox registry and updates the
|
|
6233
|
+
* sandbox-scoped live catalog.
|
|
5991
6234
|
*/
|
|
5992
6235
|
async unregisterEffect(sandboxNameOrId, name) {
|
|
5993
6236
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
5994
6237
|
const sandboxId = sandbox.sandboxId;
|
|
5995
|
-
const
|
|
5996
|
-
if (
|
|
5997
|
-
|
|
6238
|
+
const currentMap = this.sandboxEffects.get(sandboxId);
|
|
6239
|
+
if (!currentMap) {
|
|
6240
|
+
return;
|
|
5998
6241
|
}
|
|
5999
|
-
const
|
|
6000
|
-
|
|
6001
|
-
|
|
6242
|
+
const nextEntries = Array.from(currentMap.entries()).filter(
|
|
6243
|
+
([effectKey, effect]) => effectKey !== name && effect.name !== name
|
|
6244
|
+
);
|
|
6245
|
+
if (nextEntries.length === currentMap.size) {
|
|
6246
|
+
return;
|
|
6002
6247
|
}
|
|
6248
|
+
const nextMap = new Map(nextEntries);
|
|
6249
|
+
this.sandboxEffects.set(sandboxId, nextMap);
|
|
6250
|
+
if (nextMap.size === 0) {
|
|
6251
|
+
this.disconnectSandboxEffectHost(sandboxId);
|
|
6252
|
+
this.sandboxEffects.delete(sandboxId);
|
|
6253
|
+
return;
|
|
6254
|
+
}
|
|
6255
|
+
await this.syncSandboxEffectCatalog(sandboxId);
|
|
6003
6256
|
}
|
|
6004
6257
|
/**
|
|
6005
6258
|
* Unregister all effects for a sandbox.
|
|
@@ -6008,10 +6261,7 @@ var Granular = class {
|
|
|
6008
6261
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
6009
6262
|
const sandboxId = sandbox.sandboxId;
|
|
6010
6263
|
this.sandboxEffects.delete(sandboxId);
|
|
6011
|
-
|
|
6012
|
-
for (const env of activeEnvs) {
|
|
6013
|
-
await env.unpublishAllEffects();
|
|
6014
|
-
}
|
|
6264
|
+
this.disconnectSandboxEffectHost(sandboxId);
|
|
6015
6265
|
}
|
|
6016
6266
|
/**
|
|
6017
6267
|
* Find a sandbox by name or create it if it doesn't exist
|
|
@@ -6047,7 +6297,7 @@ var Granular = class {
|
|
|
6047
6297
|
const created = await this.permissionProfiles.create(sandboxId, {
|
|
6048
6298
|
name: profileName,
|
|
6049
6299
|
rules: {
|
|
6050
|
-
|
|
6300
|
+
effects: { allow: ["*"] },
|
|
6051
6301
|
resources: { allow: ["*"] }
|
|
6052
6302
|
}
|
|
6053
6303
|
});
|