@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
- ...(defaults.laneId ? { 'x-ziggs-lane': defaults.laneId } : {}),
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 ? { 'x-ziggs-lane': 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 end-user
346
- // per agreement (delegation), so sessions must not be shared across them.
347
- const cacheKey = `${userId}|${onBehalfOfAgreementId ?? ''}`;
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 operatorKey = ctx['operatorKey'];
31
- const agentId = ctx['agentId'];
32
- if (!operatorKey)
33
- throw new Error('operatorKey missing from tool context');
34
- if (!agentId)
35
- throw new Error('agentId missing from tool context');
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: ctx['operatorKey'],
68
- agentId: ctx['agentId'],
78
+ operatorKey: creds.operatorKey,
79
+ agentId: creds.agentId,
69
80
  }, {
70
- agentId: ctx['agentId'],
71
- laneId: ctx['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.17.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.16.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",