@ziggs-ai/agent-sdk 0.17.0 → 0.18.0
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.
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { resolveWebAppOrigin, connectionsSettingsAppUrl } from '@ziggs-ai/api-client';
|
|
1
|
+
import { resolveWebAppOrigin, connectionsSettingsAppUrl, laneHeader } from '@ziggs-ai/api-client';
|
|
2
2
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
3
3
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
4
4
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
5
5
|
import { ConnectionsClient, getBackendUrl } from '@ziggs-ai/api-client';
|
|
6
6
|
import { createAgent } from '../Agent.js';
|
|
7
|
+
import { laneFromToolContext } from '../tools/toolCreds.js';
|
|
7
8
|
import { thinkingDefaults } from '../runtime/defaults.js';
|
|
8
9
|
/**
|
|
9
10
|
* Typed failure from the Ziggs MCP gateway. `code` is the stable
|
|
@@ -75,8 +76,9 @@ export function resolveZiggsGatewayTarget(config, defaults = {}) {
|
|
|
75
76
|
// The lane, so the broker can tell who this wake acts for. Until it was
|
|
76
77
|
// sent, this door was the unfenced one: the named-connector proxy states
|
|
77
78
|
// its lane, so the same account and the same grant got two different
|
|
78
|
-
// answers depending on which door the agent used.
|
|
79
|
-
|
|
79
|
+
// answers depending on which door the agent used. Built by the shared
|
|
80
|
+
// helper, because a copy of this header going missing is invisible.
|
|
81
|
+
...laneHeader(defaults.laneId),
|
|
80
82
|
},
|
|
81
83
|
};
|
|
82
84
|
}
|
|
@@ -110,7 +112,7 @@ export function resolvePerUserGatewayTarget(config, agentId, userId, onBehalfOfA
|
|
|
110
112
|
// uses this agreement to resolve the delegation root's user.
|
|
111
113
|
...(onBehalfOfAgreementId ? { 'x-on-behalf-of-agreement': onBehalfOfAgreementId } : {}),
|
|
112
114
|
// The lane, for the same reason as the path-id door above.
|
|
113
|
-
...(laneId
|
|
115
|
+
...laneHeader(laneId),
|
|
114
116
|
},
|
|
115
117
|
};
|
|
116
118
|
}
|
|
@@ -135,6 +137,15 @@ export function buildTransport(config, defaults = {}) {
|
|
|
135
137
|
});
|
|
136
138
|
}
|
|
137
139
|
if (config.type === 'ziggs') {
|
|
140
|
+
/**
|
|
141
|
+
* No lane, and there cannot be one: this client is built ONCE at boot and
|
|
142
|
+
* shared by every wake, so any lane baked into its headers would be the
|
|
143
|
+
* wrong one for all but the first. The `ziggs` transport is the legacy
|
|
144
|
+
* fleet entry (`scripts/run-mcp-fleet.ts`) — a fixed connection + grant the
|
|
145
|
+
* operator configured for itself, so the lane-less fence (its own estate)
|
|
146
|
+
* is the right answer for it. A per-lane client belongs to `ziggs-per-user`,
|
|
147
|
+
* which builds one per wake and states the lane there.
|
|
148
|
+
*/
|
|
138
149
|
const target = resolveZiggsGatewayTarget(config, defaults);
|
|
139
150
|
return new StreamableHTTPClientTransport(target.url, {
|
|
140
151
|
requestInit: { headers: target.headers },
|
|
@@ -341,14 +352,17 @@ export async function createMcpAgent(options) {
|
|
|
341
352
|
}
|
|
342
353
|
const tools = capTools(toolSchema, agentId, maxTools, extraTools.length, allowedTools);
|
|
343
354
|
const clients = new Map();
|
|
344
|
-
const clientFor = (userId, onBehalfOfAgreementId) => {
|
|
345
|
-
// Key by user + agreement: the gateway may resolve a different
|
|
346
|
-
// per agreement (delegation),
|
|
347
|
-
|
|
355
|
+
const clientFor = (userId, onBehalfOfAgreementId, laneId) => {
|
|
356
|
+
// Key by user + agreement + lane: the gateway may resolve a different
|
|
357
|
+
// end-user per agreement (delegation), and the lane decides which party's
|
|
358
|
+
// grants this session may spend, so sessions must not be shared across
|
|
359
|
+
// any of the three. The headers are fixed when the transport is built,
|
|
360
|
+
// so a session reused across lanes would carry the first lane's fence.
|
|
361
|
+
const cacheKey = `${userId}|${onBehalfOfAgreementId ?? ''}|${laneId ?? ''}`;
|
|
348
362
|
let p = clients.get(cacheKey);
|
|
349
363
|
if (!p) {
|
|
350
364
|
p = (async () => {
|
|
351
|
-
const target = resolvePerUserGatewayTarget(transportConfig, agentId, userId, onBehalfOfAgreementId);
|
|
365
|
+
const target = resolvePerUserGatewayTarget(transportConfig, agentId, userId, onBehalfOfAgreementId, laneId);
|
|
352
366
|
const transport = new StreamableHTTPClientTransport(target.url, {
|
|
353
367
|
requestInit: { headers: target.headers },
|
|
354
368
|
fetch: wrapGatewayFetch(),
|
|
@@ -410,6 +424,14 @@ export async function createMcpAgent(options) {
|
|
|
410
424
|
// connection-owning end-user; pass the agreement so the gateway resolves
|
|
411
425
|
// the delegation root's user.
|
|
412
426
|
const onBehalfOfAgreementId = typeof context.onBehalfOfAgreementId === 'string' ? context.onBehalfOfAgreementId : undefined;
|
|
427
|
+
/**
|
|
428
|
+
* The lane this wake runs in, read the same way every tool reads it.
|
|
429
|
+
* Without it the relay states no lane, the gateway resolves the wake to
|
|
430
|
+
* the agent's own operator, and any grant the CUSTOMER gave is refused —
|
|
431
|
+
* on the door built for exactly that case. Omitting a lane can only
|
|
432
|
+
* narrow reach, never widen it, so the miss never raises.
|
|
433
|
+
*/
|
|
434
|
+
const laneId = laneFromToolContext(context);
|
|
413
435
|
// (B): per-user resolution is intermittently flaky even when the
|
|
414
436
|
// connection is valid and resolvable — the same on-behalf call fails with
|
|
415
437
|
// NO_USER_CONNECTION on one wake and succeeds on the next. clientFor never
|
|
@@ -420,7 +442,7 @@ export async function createMcpAgent(options) {
|
|
|
420
442
|
let lastErr;
|
|
421
443
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
422
444
|
try {
|
|
423
|
-
const c = await clientFor(userId, onBehalfOfAgreementId);
|
|
445
|
+
const c = await clientFor(userId, onBehalfOfAgreementId, laneId);
|
|
424
446
|
return (await c.callTool({ name, arguments: args }));
|
|
425
447
|
}
|
|
426
448
|
catch (err) {
|
|
@@ -2,6 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
|
2
2
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
3
3
|
import { ConnectionsClient } from '@ziggs-ai/api-client';
|
|
4
4
|
import { defineTool } from '../../../tools/defineTool.js';
|
|
5
|
+
import { credsFromToolContext } from '../../../tools/toolCreds.js';
|
|
5
6
|
import { resolveZiggsGatewayTarget } from '../../../mcp/createMcpAgent.js';
|
|
6
7
|
/**
|
|
7
8
|
* Calling a tool on a connected MCP server, one shot.
|
|
@@ -24,19 +25,20 @@ import { resolveZiggsGatewayTarget } from '../../../mcp/createMcpAgent.js';
|
|
|
24
25
|
* no argument to guess; several means the caller must name one, because picking
|
|
25
26
|
* for them is how an agent reads the wrong workspace and reports it as fact.
|
|
26
27
|
*/
|
|
27
|
-
async function resolveTarget(ctx, connectionId, grantId) {
|
|
28
|
+
async function resolveTarget(ctx, toolName, connectionId, grantId) {
|
|
28
29
|
if (connectionId && grantId)
|
|
29
30
|
return { connectionId, grantId };
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
const creds = credsFromToolContext(ctx, toolName);
|
|
32
|
+
// The lane belongs here as much as on the call itself: the grant list answers
|
|
33
|
+
// for the party the wake acts for, so a lane-less list is the agent's OWN
|
|
34
|
+
// grants only — and this is the path that PICKS one when the caller named no
|
|
35
|
+
// connection. Without it a hired agent discovers nothing and reports "no live
|
|
36
|
+
// connection grant" while holding its customer's.
|
|
37
|
+
//
|
|
36
38
|
// No explicit baseUrl: ConnectionsClient already resolves it the way the fleet
|
|
37
39
|
// configures it (HTTP_URL). Passing BACKEND_URL here was the same mistake as in
|
|
38
40
|
// the transport — undefined in deployment.
|
|
39
|
-
const groups = await new ConnectionsClient(operatorKey, agentId).listForHolder();
|
|
41
|
+
const groups = await new ConnectionsClient(creds.operatorKey, creds.agentId, undefined, creds.laneId).listForHolder();
|
|
40
42
|
const live = groups.filter((g) => (g.grants ?? []).length > 0);
|
|
41
43
|
if (live.length === 0) {
|
|
42
44
|
throw new Error('No live connection grant. Ask the connection owner to issue one for this agent, then retry.');
|
|
@@ -59,16 +61,25 @@ async function resolveTarget(ctx, connectionId, grantId) {
|
|
|
59
61
|
}
|
|
60
62
|
return { connectionId: chosen.connectionId, grantId: grant.grantId };
|
|
61
63
|
}
|
|
62
|
-
async function withClient(ctx, connectionId, grantId, run) {
|
|
64
|
+
async function withClient(ctx, toolName, connectionId, grantId, run) {
|
|
65
|
+
/**
|
|
66
|
+
* The lane comes from `credsFromToolContext`, the same reader every other
|
|
67
|
+
* tool uses. Reading it off a field of its own (`ctx.laneId`) is how this
|
|
68
|
+
* door came to send no lane at all: the executor states the lane as
|
|
69
|
+
* `chatId` / `sessionId`, so a key by any other name is always undefined,
|
|
70
|
+
* and the gateway then resolves the wake to the agent's own operator and
|
|
71
|
+
* refuses every grant its customer gave it.
|
|
72
|
+
*/
|
|
73
|
+
const creds = credsFromToolContext(ctx, toolName);
|
|
63
74
|
const target = resolveZiggsGatewayTarget({
|
|
64
75
|
type: 'ziggs',
|
|
65
76
|
connectionId,
|
|
66
77
|
grantId,
|
|
67
|
-
operatorKey:
|
|
68
|
-
agentId:
|
|
78
|
+
operatorKey: creds.operatorKey,
|
|
79
|
+
agentId: creds.agentId,
|
|
69
80
|
}, {
|
|
70
|
-
agentId:
|
|
71
|
-
laneId:
|
|
81
|
+
agentId: creds.agentId,
|
|
82
|
+
laneId: creds.laneId,
|
|
72
83
|
});
|
|
73
84
|
const transport = new StreamableHTTPClientTransport(target.url, {
|
|
74
85
|
requestInit: { headers: target.headers },
|
|
@@ -98,7 +109,7 @@ export const mcpToolsListTool = defineTool('mcp_tools_list', {
|
|
|
98
109
|
description: 'Tool names to return the FULL parameters (inputSchema) for. Omit to get the names and descriptions only — a whole server of schemas is tens of thousands of characters and gets cut, which is worse than not asking.',
|
|
99
110
|
},
|
|
100
111
|
}, async (args, ctx) => {
|
|
101
|
-
const t = await resolveTarget(ctx, args['connectionId'], args['grantId']);
|
|
112
|
+
const t = await resolveTarget(ctx, 'mcp_tools_list', args['connectionId'], args['grantId']);
|
|
102
113
|
/**
|
|
103
114
|
* Names first, schemas on demand — the same two-step this platform's own MCP
|
|
104
115
|
* surface settled on. One Linear server answers 53 tools and 78,000
|
|
@@ -107,7 +118,7 @@ export const mcpToolsListTool = defineTool('mcp_tools_list', {
|
|
|
107
118
|
* that survives still looks complete.
|
|
108
119
|
*/
|
|
109
120
|
const wanted = args['names']?.filter((n) => typeof n === 'string' && n);
|
|
110
|
-
const tools = await withClient(ctx, t.connectionId, t.grantId, async (c) => {
|
|
121
|
+
const tools = await withClient(ctx, 'mcp_tools_list', t.connectionId, t.grantId, async (c) => {
|
|
111
122
|
const res = await c.listTools();
|
|
112
123
|
const rows = res.tools ?? [];
|
|
113
124
|
const picked = wanted?.length ? rows.filter((tool) => wanted.includes(tool.name)) : rows;
|
|
@@ -253,8 +264,8 @@ export const mcpToolCallTool = defineTool('mcp_tool_call', {
|
|
|
253
264
|
const name = args['tool'];
|
|
254
265
|
if (!name)
|
|
255
266
|
throw new Error('tool is required');
|
|
256
|
-
const t = await resolveTarget(ctx, args['connectionId'], args['grantId']);
|
|
257
|
-
const raw = await withClient(ctx, t.connectionId, t.grantId, (c) => c.callTool({
|
|
267
|
+
const t = await resolveTarget(ctx, 'mcp_tools_list', args['connectionId'], args['grantId']);
|
|
268
|
+
const raw = await withClient(ctx, 'mcp_tool_call', t.connectionId, t.grantId, (c) => c.callTool({
|
|
258
269
|
name,
|
|
259
270
|
arguments: args['args'] ?? {},
|
|
260
271
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Agent framework SDK for building autonomous agents on the Ziggs platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@anthropic-ai/claude-agent-sdk": "^0.3.204",
|
|
27
27
|
"@anthropic-ai/sdk": "^0.110.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
|
-
"@ziggs-ai/api-client": "0.
|
|
29
|
+
"@ziggs-ai/api-client": "0.17.0",
|
|
30
30
|
"ajv": "^8.17.1",
|
|
31
31
|
"dotenv": "^17.2.3",
|
|
32
32
|
"openai": "^4.0.0",
|