@ziggs-ai/api-client 0.30.3 → 0.32.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.
Files changed (36) hide show
  1. package/dist/capabilities/agreementVerbs.js +1 -1
  2. package/dist/capabilities/chat.js +2 -2
  3. package/dist/capabilities/connections.js +22 -7
  4. package/dist/capabilities/context.js +25 -18
  5. package/dist/capabilities/tasks.js +2 -2
  6. package/dist/capabilities/types.d.ts +0 -16
  7. package/dist/capabilities/types.js +19 -2
  8. package/dist/decisionWords.d.ts +10 -0
  9. package/dist/decisionWords.js +16 -1
  10. package/dist/http/AgreementClient.js +2 -1
  11. package/dist/http/AssistantConnectionClient.d.ts +27 -0
  12. package/dist/http/AssistantConnectionClient.js +49 -0
  13. package/dist/http/ChatClient.js +2 -1
  14. package/dist/http/ConnectionsClient.d.ts +7 -1
  15. package/dist/http/ConnectionsClient.js +6 -5
  16. package/dist/http/ContextGrantsClient.d.ts +26 -3
  17. package/dist/http/ContextGrantsClient.js +9 -2
  18. package/dist/http/MarketplaceClient.js +2 -1
  19. package/dist/http/MessagesClient.js +4 -3
  20. package/dist/http/OrgsClient.js +0 -1
  21. package/dist/http/TaskClient.js +2 -1
  22. package/dist/http/ToolPickClient.d.ts +25 -0
  23. package/dist/http/ToolPickClient.js +35 -0
  24. package/dist/http/index.d.ts +1 -0
  25. package/dist/http/index.js +1 -0
  26. package/dist/http/operatorHeaders.d.ts +8 -0
  27. package/dist/http/operatorHeaders.js +15 -0
  28. package/dist/inboxAck.d.ts +2 -1
  29. package/dist/inboxAck.js +6 -6
  30. package/dist/index.d.ts +4 -1
  31. package/dist/index.js +4 -1
  32. package/dist/shared/contextAccessRequest.d.ts +12 -0
  33. package/dist/shared/contextAccessRequest.js +6 -0
  34. package/dist/shared/operatorKey.d.ts +1 -0
  35. package/dist/shared/operatorKey.js +1 -0
  36. package/package.json +1 -1
@@ -214,7 +214,7 @@ export const agreementBidCapability = {
214
214
  proposedTo: args['counterparty'],
215
215
  chatId: args['chatId'],
216
216
  // You provide, so the counterparty pays.
217
- providerId: creds.agentId,
217
+ providerId: creds.agentId || undefined,
218
218
  engagementKind: engagementKindFrom(args),
219
219
  }, creds);
220
220
  return { agreement };
@@ -138,7 +138,7 @@ export const openConversationCapability = {
138
138
  return {
139
139
  chatId,
140
140
  appUrl: chatAppUrl(webAppOrigin(env), chatId),
141
- actingAgentId: agentId,
141
+ ...(agentId ? { actingAgentId: agentId } : {}),
142
142
  people,
143
143
  pending,
144
144
  ...(typeof reused === 'boolean' ? { reused } : {}),
@@ -158,7 +158,7 @@ function roomRoster(args, invited, agentId) {
158
158
  const pending = (invited ?? []).filter((row) => row.status === 'pending').map((row) => row.id);
159
159
  const waiting = new Set(pending);
160
160
  const here = (invited ?? []).filter((row) => row.status !== 'pending').map((row) => row.id);
161
- const people = [...new Set([...named.filter((id) => !waiting.has(id)), ...here, agentId])];
161
+ const people = [...new Set([...named.filter((id) => !waiting.has(id)), ...here, ...(agentId ? [agentId] : [])])];
162
162
  return { people, pending };
163
163
  }
164
164
  /** Say what happened to each invited id, so the agent reports it truthfully. */
@@ -72,13 +72,15 @@ export const connectionProxyCapability = {
72
72
  export const requestConnectionCapability = {
73
73
  key: "connection_request",
74
74
  names: { sdk: "connection_request", mcp: "ziggs_connection_request" },
75
- title: "Ask your principal for a connection",
75
+ title: "Ask someone for a connection",
76
76
  descriptions: {
77
- sdk: "Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. " +
78
- "Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement. " +
77
+ sdk: "Ask a person to connect a remote MCP server and grant you the listed tools. " +
78
+ "Name who to ask in recipientId: your work brief offers them for this wake — workBrief.actingFor.customer.id (the customer of the hire you are working in, for their own accounts and resources) or workBrief.actingFor.operator.id (your operator, for tools you bring). Anyone else is refused, and the refusal lists who you can ask. " +
79
+ "Opens a connection-consent card in the chat you pass; the answer says who got it and where they approve it — tell that person, not whoever happens to be in the room. " +
79
80
  "On approval the server is connected (browser OAuth if needed) and you are granted the tools; call them with mcp_tool_call / mcp_tools_list (not connection_proxy).",
80
- mcp: "Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. " +
81
- "Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement (there is no MCP tool to approve it, so tell them to approve it in the chat). " +
81
+ mcp: "Ask a person to connect a remote MCP server and grant you the listed tools. " +
82
+ "Name who to ask in recipientId: the person you are helping (your own account) unless the server says otherwise; a pick it cannot use here is refused, and the refusal lists who you can ask. " +
83
+ "Opens a connection-consent card in the chat you pass; the answer says who got it and where they approve it (there is no MCP tool to approve it). " +
82
84
  "On approval the server is connected (browser OAuth if needed) and you are granted the tools; the result shows up in ziggs_connection_list for use with ziggs_mcp_tools_list / ziggs_mcp_tool_call.",
83
85
  },
84
86
  annotation: "write",
@@ -103,6 +105,11 @@ export const requestConnectionCapability = {
103
105
  type: "string",
104
106
  description: "Plain-language reason shown to the human deciding",
105
107
  },
108
+ recipientId: {
109
+ type: "string",
110
+ required: true,
111
+ description: "Who to ask: workBrief.actingFor.customer.id (the customer of this hire, for their own accounts) or workBrief.actingFor.operator.id (your operator, for tools you bring)",
112
+ },
106
113
  },
107
114
  needsAgentId: true,
108
115
  handler: async (args, env) => {
@@ -114,20 +121,28 @@ export const requestConnectionCapability = {
114
121
  if (!Array.isArray(tools) || tools.length === 0) {
115
122
  throw new Error("tools must be a non-empty array of tool names");
116
123
  }
124
+ if (typeof args["recipientId"] !== "string" || !args["recipientId"].trim())
125
+ throw new Error("recipientId is required: who to ask, from your work brief");
117
126
  try {
118
127
  const result = await client(env).requestMcpConnection({
119
128
  chatId: args["chatId"],
120
129
  serverUrl: args["serverUrl"],
121
130
  tools: tools,
122
131
  reason: args["reason"],
132
+ recipientId: args["recipientId"].trim(),
123
133
  });
134
+ // Who got the card is the server's answer; say that, never "your
135
+ // principal" — the card often goes to someone else.
136
+ const sent = typeof result["message"] === "string"
137
+ ? result["message"]
138
+ : "A connection-consent card is open.";
124
139
  return {
125
140
  ok: true,
126
141
  ...result,
127
142
  note: env.surface === "mcp"
128
- ? "A connection-consent card is now in the chat awaiting your principal. Tell the human now (pull-only MCP has no push) — they approve it right in the chat. " +
143
+ ? `${sent} Tell that person now (pull-only MCP has no push). ` +
129
144
  "Once approved, the connection + grant appear in ziggs_connection_list for ziggs_mcp_tools_list / ziggs_mcp_tool_call."
130
- : "A connection-consent card is now in the chat awaiting your principal — they approve it right there. " +
145
+ : `${sent} Tell that person; only they can approve it. ` +
131
146
  "Once approved, the connection + grant appear in grant_list (scopeKind [\"connection\"]) / connection_list_grants for mcp_tools_list / mcp_tool_call.",
132
147
  };
133
148
  }
@@ -1,3 +1,4 @@
1
+ import { isContextAccessRequest } from '../shared/contextAccessRequest.js';
1
2
  import { presentResourceActions } from './resourceActions.js';
2
3
  import { ContextReadClient, CONTEXT_READ_TYPES, CONTEXT_READ_VIA, parseVia, viaHint, } from '../http/ContextReadClient.js';
3
4
  import { ContextOpenClient } from '../http/ContextOpenClient.js';
@@ -65,7 +66,7 @@ export const contextReadCapability = {
65
66
  title: 'Read context you hold',
66
67
  descriptions: {
67
68
  sdk: `Read the contents of a scope you already hold a grant for: ${CONTEXT_READ_TYPES.join(' | ')}. Each type reads through its own entries — ${VIA_BY_TYPE}. Use grant_list first to see which scopes your grants cover, then read through any of them. ${ARTIFACT_VIA_NOTE}. Cursored; all access is grant-fenced server-side.`,
68
- mcp: `Read the contents of a scope you already hold: ${CONTEXT_READ_TYPES.join(' | ')} (the type param). Each type accepts its own via entries — ${VIA_BY_TYPE} — and any other pairing is refused. ${ARTIFACT_VIA_NOTE} (e.g. an artifact someone shared with you; ziggs_grant_list with scopeKind ["artifact"] shows those). Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a \`readPlan\` with the next page and/or forward-delta call pre-filled (after=this page's latestSequence), so you can keep reading without rebuilding args. This is the single read path for all four types — to discover which scopes exist, use the listers: ziggs_chat_list, ziggs_task_list, ziggs_agreement_list, ziggs_grant_list, ziggs_link_list.`,
69
+ mcp: `Read the contents of a scope you already hold: ${CONTEXT_READ_TYPES.join(' | ')} (the type param). Each type accepts its own via entries — ${VIA_BY_TYPE} — and any other pairing is refused. ${ARTIFACT_VIA_NOTE} (e.g. an artifact someone shared with you; ziggs_grant_list with scopeKind ["artifact"] shows those). Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a \`readPlan\` with the next page and/or forward-delta call pre-filled (after=this page's latestSequence), so you can keep reading without rebuilding args. This is the single read path for all context types — to discover which scopes exist, use the listers: ziggs_chat_list, ziggs_task_list, ziggs_agreement_list, ziggs_grant_list, ziggs_link_list.`,
69
70
  },
70
71
  annotation: 'read-only',
71
72
  params: {
@@ -301,7 +302,7 @@ function presentOpenResult(result, env) {
301
302
  return [];
302
303
  const row = item;
303
304
  const ask = row.grantRequest;
304
- if (row.status !== 'active' || ask?.primitive !== 'context' || ask.kind !== 'access-request' || ask.holderId !== fullCreds(env).agentId || !ask.scope?.id || !['artifact', 'chat', 'agreement', 'task'].includes(ask.scope.kind ?? ''))
305
+ if (row.status !== 'active' || !isContextAccessRequest(ask) || ask.holderId !== fullCreds(env).agentId || !ask.scope?.id || !['artifact', 'chat', 'agreement', 'task'].includes(ask.scope.kind ?? ''))
305
306
  return [];
306
307
  return [{ kind: 'resume', tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: { [`${ask.scope.kind}Id`]: ask.scope.id }, effect: 'read_only', note: 'Approval recorded. Open the original scope; the server rechecks expiry and revocation.' }];
307
308
  });
@@ -368,13 +369,13 @@ export const contextRequestCapability = {
368
369
  title: 'Ask an owner for artifact access',
369
370
  descriptions: {
370
371
  sdk: 'Ask a human owner for temporary read-only access to one artifact you were given a reference for. Use your existing chat with that owner. Creates an approval agreement; gives no access until the owner approves. You are the only recipient. After approval, open the original artifact. Never use a generic service agreement as a substitute for this access request.',
371
- mcp: 'Request access to a restricted artifact directly from its human owner. Pass the artifact id you were given, your conversation with the owner, and the owner account id or participant reference shown in that chat. This creates a pending approval agreement, NOT a grant. No prior grant is needed. The human approves at the agreement link. After approval, ziggs_open the original artifact. Scope is read-only, one artifact, for 1–168 hours (default 24); no other artifacts or chats. Include the intended summary recipient in reason. Keep polling for the decision when asked to wait. A request does not confirm that the artifact exists or belongs to the named owner; ownership is checked at approval.',
372
+ mcp: 'Request access to a restricted artifact directly from its human owner. Pass the artifact id you were given, your conversation with the owner, and in ownerId who to ask (a pick the server cannot use here is refused with the list). This creates a pending approval agreement, NOT a grant. No prior grant is needed. The human approves at the agreement link. After approval, ziggs_open the original artifact. Scope is read-only, one artifact, for 1–168 hours (default 24); no other artifacts or chats. Include the intended summary recipient in reason. Keep polling for the decision when asked to wait. A request does not confirm that the artifact exists or belongs to the named owner; ownership is checked at approval.',
372
373
  },
373
374
  annotation: 'write', needsAgentId: true,
374
375
  params: {
375
376
  artifactId: { type: 'string', required: true },
376
377
  chatId: { type: 'string', required: true },
377
- ownerId: { type: 'string', required: true, description: 'Human account id or room participant reference, not a persona face' },
378
+ ownerId: { type: 'string', required: true, description: 'Who to ask, from your work brief: workBrief.actingFor.customer.id or workBrief.actingFor.operator.id. Anyone else is refused, and the refusal lists who you can ask.' },
378
379
  reason: { type: 'string', required: true, description: 'Purpose, including whom you will summarize the artifact to' },
379
380
  durationHours: { type: 'number', description: 'Integer 1–168 hours after approval; default 24' },
380
381
  idempotencyKey: { type: 'string', description: 'Reuse for the same request; do not create duplicate approval cards' },
@@ -390,7 +391,7 @@ export const contextRequestCapability = {
390
391
  const result = await new ContextGrantsClient(creds.operatorKey, creds.agentId, env.baseUrl).requestArtifactAccess(args.artifactId, {
391
392
  chatId: args.chatId, ownerId: args.ownerId, reason: args.reason, durationHours,
392
393
  }, { idempotencyKey: args.idempotencyKey, laneId: creds.laneId });
393
- return { ...result, outcome: 'pending', state: 'requested', appUrl: `${(env.webUrl ?? 'https://ziggsai.com').replace(/\/$/, '')}/app/agreements/${encodeURIComponent(result.agreementId)}`, note: 'Waiting for the artifact owner. No access granted yet; open the original artifact only after approval.' };
394
+ return { ...result, outcome: 'pending', state: 'requested', appUrl: `${(env.webUrl ?? 'https://ziggsai.com').replace(/\/$/, '')}/app/agreements/${encodeURIComponent(result.agreementId)}`, note: `${result.message ?? 'Waiting for the artifact owner.'} No access granted yet; open the original artifact only after approval.` };
394
395
  },
395
396
  };
396
397
  /** A suggested request still needs a purpose; it is never a grant or an approval. */
@@ -398,7 +399,7 @@ function requestAccessAction(args, env) {
398
399
  return {
399
400
  kind: 'request_access',
400
401
  tool: env.surface === 'mcp' ? 'ziggs_access_request' : 'access_request',
401
- args, requiredInput: args.scopeKind === 'artifact' ? ['reason', 'chatId', 'ownerId'] : ['reason'], effect: 'request_only',
402
+ args, requiredInput: args.scopeKind === 'artifact' ? ['reason', 'chatId', 'recipientId'] : ['reason', 'recipientId'], effect: 'request_only',
402
403
  requiresApproval: true,
403
404
  };
404
405
  }
@@ -406,30 +407,35 @@ export const accessRequestCapability = {
406
407
  key: 'access_request',
407
408
  names: { sdk: 'access_request', mcp: 'ziggs_access_request' },
408
409
  title: 'Request access for yourself',
410
+ annotation: 'write', needsAgentId: true,
409
411
  descriptions: {
410
- sdk: 'Create a pending approval agreement for the calling agent to access one artifact, chat, agreement context, or task branch for 1–168 hours. This only requests permission: it never grants access, signs, or approves. After the authorized person approves, open the original resource. Artifact requests need a writable conversation with the named owner; other requests go to your accountable person, who must have authority over the scope. Read-only by default; permission=reply requests read and reply to one chat. No spending or work commitment. Reuse idempotencyKey for a retry.',
411
- mcp: 'Ask for missing access through a pending approval agreement. Use this when open/access_explain reports an unreadable resource, or discovery/inbox names a scope you need. It requests access ONLY for this MCP agent; never grants access or approves anything. Scope is one artifact, chat, agreement context, or task branch (including subtasks), for 1–168 hours (default 24). Read-only by default; permission=reply requests read and reply in one chat. For earlier chat messages explicitly set temporal=from-start. Artifact requests require chatId of your existing writable conversation with the owner and that ownerId; other requests route to your accountable person, who must be allowed to share the resource. Surface appUrl, wait for the decision when asked, then open the original resource and resume. Pending grants no access. Never use issue_grant, change identity, or form a generic work agreement to recover missing access.',
412
+ sdk: 'Request optional bounded access to one artifact, chat, agreement context or task. Creates only a pending approval; never grants access or approves. Name who to ask in recipientId: your work brief offers them for this wake — workBrief.actingFor.customer.id (the customer of the hire you are working in, for their own accounts and resources) or workBrief.actingFor.operator.id (your operator, for tools you bring). Anyone else is refused, and the refusal lists who you can ask. Artifact requests also need chatId, the conversation they approve in. Use scopeId for a known artifact; or scopeKind=artifact with agreementId of your active hire to request its system-maintained workspace summary (workspace name/type and agents, including future updates, not private conversations or document bodies). Send the returned appUrl, wait for human approval, then open the returned scope. Hiring alone grants no source access. Revocation and expiry are checked by the system.',
413
+ mcp: 'Request access for this agent, never approve or grant it. Use scopeId for an artifact, chat, agreement or task; scopeKind=artifact with agreementId of your active hire resolves its system-maintained workspace summary artifact. Name who to ask in recipientId; a pick the server cannot use here is refused, and the refusal lists who you can ask. Artifact requests also require chatId. Explain the scope and lifetime; summary access includes future updates only to that artifact. Surface appUrl and wait for human approval; then open the returned scope. Pending grants no access.',
412
414
  },
413
- annotation: 'write', needsAgentId: true,
414
415
  params: {
415
416
  scopeKind: { type: 'string', required: true, enum: ['artifact', 'chat', 'agreement', 'task'] },
416
- scopeId: { type: 'string', required: true, description: 'Original returned resource id; this request does not open a new resource' },
417
+ scopeId: { type: 'string', description: 'Original resource id. Omit when using a hire agreementId to request its workspace summary.' },
418
+ agreementId: { type: 'string', description: 'Artifact scope only: current hire id, used instead of scopeId to request the system-maintained workspace summary.' },
417
419
  reason: { type: 'string', required: true, description: 'Why access is needed and who will receive the reply or summary' },
418
420
  permission: { type: 'string', enum: ['read', 'reply'], description: 'read by default; reply is read and reply and is only valid for a chat' },
419
421
  temporal: { type: 'string', enum: ['from-now', 'from-start'], description: 'Chat history requested; default from-now. Earlier messages need from-start. Other resources use from-start.' },
420
422
  durationHours: { type: 'number', description: 'Integer 1–168 hours after approval; default 24' },
421
- chatId: { type: 'string', description: 'Artifact requests only: existing conversation with the owner' },
422
- ownerId: { type: 'string', description: 'Artifact requests only: human account or participant reference in that conversation' },
423
+ chatId: { type: 'string', description: 'Artifact or workspace requests only: existing conversation with the owner' },
424
+ recipientId: { type: 'string', required: true, description: 'Who to ask: workBrief.actingFor.customer.id (the customer of this hire) or workBrief.actingFor.operator.id (your operator)' },
423
425
  idempotencyKey: { type: 'string', description: 'Reuse for the same logical request' },
424
426
  },
425
427
  handler: async (args, env) => {
426
- for (const key of ['scopeKind', 'scopeId', 'reason']) {
428
+ for (const key of ['scopeKind', 'reason', 'recipientId']) {
427
429
  if (typeof args[key] !== 'string' || !String(args[key]).trim())
428
430
  throw new Error(`${key} is required`);
429
431
  }
430
432
  const scopeKind = args.scopeKind;
431
433
  if (!['artifact', 'chat', 'agreement', 'task'].includes(scopeKind))
432
434
  throw new Error('Unsupported scopeKind');
435
+ const agreementId = typeof args.agreementId === 'string' ? args.agreementId.trim() : undefined;
436
+ const scopeId = typeof args.scopeId === 'string' ? args.scopeId.trim() : undefined;
437
+ if (Boolean(agreementId) === Boolean(scopeId) || (agreementId && scopeKind !== 'artifact'))
438
+ throw new Error('Provide scopeId, or agreementId for its workspace summary artifact');
433
439
  const permission = args.permission ?? 'read';
434
440
  if (!['read', 'reply'].includes(String(permission)) || (permission === 'reply' && scopeKind !== 'chat'))
435
441
  throw new Error('Only chat access may request reply permission');
@@ -440,16 +446,17 @@ export const accessRequestCapability = {
440
446
  throw new Error('durationHours must be an integer between 1 and 168');
441
447
  const creds = fullCreds(env);
442
448
  const result = await new ContextGrantsClient(creds.operatorKey, creds.agentId, env.baseUrl).requestAccess({
443
- scopeKind, scopeId: String(args.scopeId).trim(), reason: String(args.reason).trim(),
449
+ scopeKind, scopeId, agreementId, reason: String(args.reason).trim(),
444
450
  permission: permission, temporal: args.temporal,
445
- durationHours, chatId: args.chatId, ownerId: args.ownerId,
451
+ durationHours, chatId: args.chatId,
452
+ recipientId: typeof args.recipientId === 'string' ? args.recipientId.trim() : undefined,
446
453
  }, { idempotencyKey: args.idempotencyKey, laneId: creds.laneId });
447
454
  return {
448
455
  ...result, outcome: 'pending', state: 'requested', actingAgentId: creds.agentId,
449
- scope: { kind: scopeKind, id: args.scopeId },
456
+ scope: result.scope ?? { kind: scopeKind, id: scopeId },
450
457
  appUrl: `${(env.webUrl ?? 'https://ziggsai.com').replace(/\/$/, '')}/app/agreements/${encodeURIComponent(result.agreementId)}`,
451
- resumeAfterApproval: { tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: { [`${scopeKind}Id`]: args.scopeId } },
452
- note: 'No access granted. The authorized person must approve; then open the original resource using the same agent identity.',
458
+ resumeAfterApproval: { tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: result.scope ? { [`${result.scope.kind}Id`]: result.scope.id } : { [`${scopeKind}Id`]: scopeId } },
459
+ note: `${result.message ?? 'A request card is open.'} No access granted until they approve; then open the original resource using the same agent identity.`,
453
460
  };
454
461
  },
455
462
  };
@@ -48,9 +48,9 @@ export const listTasksCapability = {
48
48
  handler: async (args, env) => {
49
49
  const creds = fullCreds(env);
50
50
  const assignedTo = args['assignedToMe']
51
- ? creds.agentId
51
+ ? 'self'
52
52
  : args['assignedTo'];
53
- const createdBy = args['createdByMe'] ? creds.agentId : undefined;
53
+ const createdBy = args['createdByMe'] ? 'self' : undefined;
54
54
  const listed = await listTasks({
55
55
  state: args['state'],
56
56
  cursor: args['cursor'],
@@ -1,19 +1,3 @@
1
- /**
2
- * one shared definition per SDK/MCP tool capability.
3
- *
4
- * The tool-surface-parity epic (→903) unified the HTTP layer here in
5
- * api-client but left ~25 shared capabilities hand-written twice: once in the
6
- * agent-sdk's defineTool DSL, once in ziggs-mcp zod. Each capability below is
7
- * the single source for the tool's schema, validation, response shaping, and
8
- * guidance; the two surfaces register it through thin adapters
9
- * (agent-sdk `toolFromCapability`, ziggs-mcp `registerCapability`).
10
- *
11
- * Params are a neutral JSON-schema-flavoured DSL rather than zod because the
12
- * two surfaces cannot share one zod: agent-sdk is on zod v4, ziggs-mcp on
13
- * zod v3 (pinned by the MCP SDK's ZodRawShape), and api-client deliberately
14
- * has no zod dependency. The SDK adapter feeds params straight into
15
- * defineTool's converter; the MCP adapter lowers them to zod v3.
16
- */
17
1
  import type { Creds } from '../types.js';
18
2
  export type CapabilitySurface = 'sdk' | 'mcp';
19
3
  /** read-only / write / destructive — lowered to MCP tool annotations. */
@@ -1,13 +1,30 @@
1
+ /**
2
+ * one shared definition per SDK/MCP tool capability.
3
+ *
4
+ * The tool-surface-parity epic (→903) unified the HTTP layer here in
5
+ * api-client but left ~25 shared capabilities hand-written twice: once in the
6
+ * agent-sdk's defineTool DSL, once in ziggs-mcp zod. Each capability below is
7
+ * the single source for the tool's schema, validation, response shaping, and
8
+ * guidance; the two surfaces register it through thin adapters
9
+ * (agent-sdk `toolFromCapability`, ziggs-mcp `registerCapability`).
10
+ *
11
+ * Params are a neutral JSON-schema-flavoured DSL rather than zod because the
12
+ * two surfaces cannot share one zod: agent-sdk is on zod v4, ziggs-mcp on
13
+ * zod v3 (pinned by the MCP SDK's ZodRawShape), and api-client deliberately
14
+ * has no zod dependency. The SDK adapter feeds params straight into
15
+ * defineTool's converter; the MCP adapter lowers them to zod v3.
16
+ */
17
+ import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
1
18
  /** Creds for impersonated calls; adapters guarantee agentId when needsAgentId. */
2
19
  export function fullCreds(env) {
3
20
  const { operatorKey, agentId, laneId } = env.creds;
4
21
  if (!operatorKey)
5
22
  throw new Error('operatorKey missing from tool context');
6
- if (!agentId)
23
+ if (!agentId && !decodeOperatorKeyClaims(operatorKey)?.connectionClientId)
7
24
  throw new Error('agentId missing from tool context');
8
25
  // the lane rides along so every Creds-based client sends
9
26
  // X-Ziggs-Lane without each capability having to remember to.
10
- return { operatorKey, agentId, ...(laneId ? { laneId } : {}) };
27
+ return { operatorKey, agentId: agentId ?? '', ...(laneId ? { laneId } : {}) };
11
28
  }
12
29
  /**
13
30
  * Re-throw a client error with a capability-level prefix, preserving the HTTP
@@ -13,6 +13,16 @@
13
13
  * counterparty wrote: no titles, no messages, no descriptions. Ids, an actor id
14
14
  * and an instant are all it carries.
15
15
  */
16
+ /**
17
+ * The actor as it may appear in words, or null.
18
+ *
19
+ * A person's account id is `username:::email`. The server shows a reader
20
+ * outside that person's org their `psn_` face instead, but an older
21
+ * server, or a row written before that, still hands the raw id over, and the
22
+ * model repeats whatever it is told into the room. So an id shaped like a
23
+ * person's account is left out rather than said.
24
+ */
25
+ export declare function speakableActor(actorId: string | null | undefined): string | null;
16
26
  export type DecisionOutcome = 'approved' | 'rejected' | 'fulfilled' | 'held';
17
27
  export interface DecisionWordsInput {
18
28
  /** The delivery row's `reason`. Anything that is not a decision maps to null. */
@@ -13,6 +13,20 @@
13
13
  * counterparty wrote: no titles, no messages, no descriptions. Ids, an actor id
14
14
  * and an instant are all it carries.
15
15
  */
16
+ /**
17
+ * The actor as it may appear in words, or null.
18
+ *
19
+ * A person's account id is `username:::email`. The server shows a reader
20
+ * outside that person's org their `psn_` face instead, but an older
21
+ * server, or a row written before that, still hands the raw id over, and the
22
+ * model repeats whatever it is told into the room. So an id shaped like a
23
+ * person's account is left out rather than said.
24
+ */
25
+ export function speakableActor(actorId) {
26
+ if (typeof actorId !== 'string' || !actorId)
27
+ return null;
28
+ return actorId.includes(':::') || actorId.includes('@') ? null : actorId;
29
+ }
16
30
  const REFUSED_REASONS = new Set([
17
31
  'approval:rejected',
18
32
  'context_request_rejected',
@@ -38,7 +52,8 @@ export function decisionWords(input) {
38
52
  const on = input.agreementId
39
53
  ? `agreement ${input.agreementId}`
40
54
  : 'this agreement';
41
- const by = input.actorId ? ` by ${input.actorId}` : '';
55
+ const actor = speakableActor(input.actorId);
56
+ const by = actor ? ` by ${actor}` : '';
42
57
  const at = input.ts ? ` at ${input.ts}` : '';
43
58
  const artifactId = input.scope?.kind === 'artifact' && input.scope.id ? input.scope.id : null;
44
59
  if (REFUSED_REASONS.has(reason)) {
@@ -1,3 +1,4 @@
1
+ import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
1
2
  import { runtimeLog } from '../shared/runtimeLog.js';
2
3
  import { getBackendUrl } from '../utils/urlUtils.js';
3
4
  import { OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, isBroadcastTarget, partySideIds } from '../types.js';
@@ -14,7 +15,7 @@ const buildHeaders = buildCredsHeaders;
14
15
  function assertCreds(creds, op) {
15
16
  if (!creds?.operatorKey)
16
17
  throw new Error(`operatorKey is required for ${op}`);
17
- if (!creds?.agentId)
18
+ if (!creds?.agentId && !decodeOperatorKeyClaims(creds.operatorKey)?.connectionClientId)
18
19
  throw new Error(`agentId is required for ${op}`);
19
20
  }
20
21
  /**
@@ -0,0 +1,27 @@
1
+ import { type Creds } from '../types.js';
2
+ export interface AssistantWorkspace {
3
+ orgId: string;
4
+ name: string;
5
+ agentId: string;
6
+ }
7
+ /**
8
+ * The reads a connection makes about itself: which workspaces it works in,
9
+ * and its one inbox across all of them. It never remembers a
10
+ * current workspace and never changes shared credentials; the per-call pick
11
+ * is the server's (see `routing.ts`).
12
+ */
13
+ export declare class AssistantConnectionClient {
14
+ private readonly creds;
15
+ private readonly baseUrl;
16
+ constructor(creds: Creds, baseUrl?: string);
17
+ request<T>(path: string, body?: unknown): Promise<T>;
18
+ /** Live: a workspace joined since connecting shows up here with its assistant. */
19
+ workspaces(): Promise<AssistantWorkspace[]>;
20
+ /** Which of the connection's assistants is on an existing thing. */
21
+ resolve(kind: string, id: string): Promise<{
22
+ workspace: AssistantWorkspace | null;
23
+ choices: AssistantWorkspace[];
24
+ }>;
25
+ inbox(waitSeconds?: number): Promise<Record<string, unknown>>;
26
+ ack(upTo: string, handledResourceIds?: string[]): Promise<unknown>;
27
+ }
@@ -0,0 +1,49 @@
1
+ import { getBackendUrl } from '../utils/urlUtils.js';
2
+ import { hostIdentity } from '../instanceIdentity.js';
3
+ import { ApiError } from '../types.js';
4
+ import { parseErrorCode, parseErrorMessage } from '../shared/apiError.js';
5
+ /**
6
+ * The reads a connection makes about itself: which workspaces it works in,
7
+ * and its one inbox across all of them. It never remembers a
8
+ * current workspace and never changes shared credentials; the per-call pick
9
+ * is the server's (see `routing.ts`).
10
+ */
11
+ export class AssistantConnectionClient {
12
+ creds;
13
+ baseUrl;
14
+ constructor(creds, baseUrl = getBackendUrl()) {
15
+ this.creds = creds;
16
+ this.baseUrl = baseUrl;
17
+ }
18
+ async request(path, body) {
19
+ const response = await fetch(`${this.baseUrl}/oauth/${path}`, {
20
+ method: body === undefined ? 'GET' : 'POST',
21
+ headers: {
22
+ Authorization: `Bearer ${this.creds.operatorKey}`,
23
+ 'Content-Type': 'application/json',
24
+ 'X-Ziggs-Instance': hostIdentity(this.creds.instanceId),
25
+ },
26
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
27
+ });
28
+ const text = await response.text();
29
+ if (!response.ok) {
30
+ throw new ApiError(parseErrorMessage(text, `${path} failed: ${response.status}`), response.status, text, parseErrorCode(text));
31
+ }
32
+ return JSON.parse(text);
33
+ }
34
+ /** Live: a workspace joined since connecting shows up here with its assistant. */
35
+ async workspaces() {
36
+ return (await this.request('workspaces'))
37
+ .workspaces;
38
+ }
39
+ /** Which of the connection's assistants is on an existing thing. */
40
+ resolve(kind, id) {
41
+ return this.request(`resolve?${new URLSearchParams({ kind, id })}`);
42
+ }
43
+ inbox(waitSeconds = 0) {
44
+ return this.request(`inbox?wait=${waitSeconds}`);
45
+ }
46
+ ack(upTo, handledResourceIds = []) {
47
+ return this.request('inbox/ack', { upTo, handledResourceIds });
48
+ }
49
+ }
@@ -1,3 +1,4 @@
1
+ import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
1
2
  import { runtimeLog } from '../shared/runtimeLog.js';
2
3
  import { getBackendUrl } from '../utils/urlUtils.js';
3
4
  import { throwApiError } from '../shared/apiError.js';
@@ -7,7 +8,7 @@ const buildHeaders = buildCredsHeaders;
7
8
  function assertCreds(creds, op) {
8
9
  if (!creds?.operatorKey)
9
10
  throw new Error(`operatorKey is required for ${op}`);
10
- if (!creds?.agentId)
11
+ if (!creds?.agentId && !decodeOperatorKeyClaims(creds.operatorKey)?.connectionClientId)
11
12
  throw new Error(`agentId is required for ${op}`);
12
13
  }
13
14
  export async function openConversation(participant, creds, { newChat = false, agreementId, createIfMissing } = {}) {
@@ -32,6 +32,12 @@ export interface McpConnectionRequestParams {
32
32
  serverUrl: string;
33
33
  tools: string[];
34
34
  reason?: string;
35
+ /**
36
+ * Who to ask, from the people the server offers this wake (the work brief's
37
+ * `actingFor.customer.id` / `.operator.id`). Required by the server; the
38
+ * per-user relay may send it as `onBehalfOfUserId` instead.
39
+ */
40
+ recipientId?: string;
35
41
  /**
36
42
  * Working chat the consent card is opened into. Required by the backend for
37
43
  * the agent-tool path; the per-user gateway trigger omits it.
@@ -119,7 +125,7 @@ export declare class ConnectionsClient {
119
125
  * connection-consent agreement in the working chat; on approval the
120
126
  * server is connected (if needed) and the agent is granted the tools.
121
127
  */
122
- requestMcpConnection({ serverUrl, tools, reason, chatId, onBehalfOfUserId, }: McpConnectionRequestParams): Promise<Record<string, unknown>>;
128
+ requestMcpConnection({ serverUrl, tools, reason, chatId, recipientId, onBehalfOfUserId, }: McpConnectionRequestParams): Promise<Record<string, unknown>>;
123
129
  private _requestRaw;
124
130
  private _request;
125
131
  }
@@ -1,3 +1,4 @@
1
+ import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
1
2
  import { getBackendUrl } from "../utils/urlUtils.js";
2
3
  import { throwApiError } from "../shared/apiError.js";
3
4
  import { buildOperatorHeaders } from "./operatorHeaders.js";
@@ -65,7 +66,7 @@ export class ConnectionsClient {
65
66
  throw new Error("proxy: grantId is required");
66
67
  if (!action)
67
68
  throw new Error("proxy: action is required");
68
- if (!this.agentId) {
69
+ if (!this.agentId && !decodeOperatorKeyClaims(this.operatorKey)?.connectionClientId) {
69
70
  throw new Error("proxy: agentId is required — connection broker calls must impersonate the grant holder agent");
70
71
  }
71
72
  const text = await this._requestRaw("POST", `/connections/${encodeURIComponent(connectionId)}/proxy`, { grantId, action, payload: payload ?? {} });
@@ -88,12 +89,12 @@ export class ConnectionsClient {
88
89
  async listGrants({ connectionId, }) {
89
90
  if (!connectionId)
90
91
  throw new Error("listGrants: connectionId is required");
91
- if (!this.agentId) {
92
+ if (!this.agentId && !decodeOperatorKeyClaims(this.operatorKey)?.connectionClientId) {
92
93
  throw new Error("listGrants: agentId is required — grants are scoped to the impersonated agent");
93
94
  }
94
95
  const res = (await this._request("GET", `/connections/${encodeURIComponent(connectionId)}/grants`, undefined));
95
96
  const grants = res["grants"] || [];
96
- return grants.filter((g) => g["holderId"] === this.agentId);
97
+ return this.agentId ? grants.filter((g) => g["holderId"] === this.agentId) : grants;
97
98
  }
98
99
  /**
99
100
  * cross-connection discovery over the unified GET /grants:
@@ -174,10 +175,10 @@ export class ConnectionsClient {
174
175
  * connection-consent agreement in the working chat; on approval the
175
176
  * server is connected (if needed) and the agent is granted the tools.
176
177
  */
177
- async requestMcpConnection({ serverUrl, tools, reason, chatId, onBehalfOfUserId, }) {
178
+ async requestMcpConnection({ serverUrl, tools, reason, chatId, recipientId, onBehalfOfUserId, }) {
178
179
  if (!serverUrl)
179
180
  throw new Error("requestMcpConnection: serverUrl is required");
180
- return (await this._request("POST", "/connections/mcp/requests", { serverUrl, tools, reason, chatId }, onBehalfOfUserId
181
+ return (await this._request("POST", "/connections/mcp/requests", { serverUrl, tools, reason, chatId, ...(recipientId ? { recipientId } : {}) }, onBehalfOfUserId
181
182
  ? { "X-On-Behalf-Of-User": onBehalfOfUserId }
182
183
  : undefined));
183
184
  }
@@ -99,13 +99,16 @@ export declare class ContextGrantsClient {
99
99
  /** Request-only endpoint: a successful call must still have granted no access. */
100
100
  requestAccess(input: {
101
101
  scopeKind: 'artifact' | 'chat' | 'agreement' | 'task';
102
- scopeId: string;
102
+ scopeId?: string;
103
+ agreementId?: string;
103
104
  reason: string;
104
105
  permission?: 'read' | 'reply';
105
106
  temporal?: 'from-now' | 'from-start';
106
107
  durationHours?: number;
107
108
  chatId?: string;
108
109
  ownerId?: string;
110
+ /** Who to ask, from the people the server offers this wake. */
111
+ recipientId?: string;
109
112
  }, opts?: {
110
113
  idempotencyKey?: string;
111
114
  laneId?: string;
@@ -113,7 +116,11 @@ export declare class ContextGrantsClient {
113
116
  status: 'pending_approval';
114
117
  agreementId: string;
115
118
  accessGranted: false;
116
- }>;
119
+ scope?: {
120
+ kind: string;
121
+ id: string;
122
+ };
123
+ } & RequestSentTo>;
117
124
  requestArtifactAccess(artifactId: string, input: {
118
125
  chatId: string;
119
126
  ownerId: string;
@@ -125,7 +132,7 @@ export declare class ContextGrantsClient {
125
132
  }): Promise<{
126
133
  status: 'pending_approval';
127
134
  agreementId: string;
128
- }>;
135
+ } & RequestSentTo>;
129
136
  delegateGrant(parentGrantId: string, input: DelegateContextGrantInput, opts?: {
130
137
  idempotencyKey?: string;
131
138
  }): Promise<DelegateContextGrantResult>;
@@ -158,3 +165,19 @@ export declare class ContextGrantsClient {
158
165
  revokedCount?: number;
159
166
  }>;
160
167
  }
168
+ /** Who got a request card and where they answer it, as the server says. */
169
+ export interface RequestSentTo {
170
+ sentTo?: {
171
+ id: string;
172
+ name: string | null;
173
+ role: 'customer' | 'operator';
174
+ };
175
+ approveIn?: {
176
+ kind: 'chat';
177
+ chatId: string;
178
+ } | {
179
+ kind: 'inbox';
180
+ };
181
+ /** e.g. "Sent to Ido. Ido approves it in this room." */
182
+ message?: string;
183
+ }
@@ -80,7 +80,7 @@ export class ContextGrantsClient {
80
80
  if (result.status !== 'pending_approval' || !result.agreementId || result.accessGranted !== false) {
81
81
  throw new Error('Expected a pending access request with accessGranted=false');
82
82
  }
83
- return { status: 'pending_approval', agreementId: result.agreementId, accessGranted: false };
83
+ return { status: 'pending_approval', agreementId: result.agreementId, accessGranted: false, ...(result.scope ? { scope: result.scope } : {}), ...sentToOf(result) };
84
84
  }
85
85
  async requestArtifactAccess(artifactId, input, opts) {
86
86
  const res = await fetch(`${this.baseUrl}/context/artifacts/${encodeURIComponent(artifactId)}/access-requests`, {
@@ -98,7 +98,7 @@ export class ContextGrantsClient {
98
98
  const result = JSON.parse(body);
99
99
  if (result.status !== 'pending_approval' || !result.agreementId)
100
100
  throw new Error('Expected a pending artifact access request');
101
- return { status: 'pending_approval', agreementId: result.agreementId };
101
+ return { status: 'pending_approval', agreementId: result.agreementId, ...sentToOf(result) };
102
102
  }
103
103
  async delegateGrant(parentGrantId, input, opts) {
104
104
  const res = await fetch(`${this.baseUrl}/context/grants/${encodeURIComponent(parentGrantId)}/delegate`, {
@@ -185,3 +185,10 @@ export class ContextGrantsClient {
185
185
  return JSON.parse(body);
186
186
  }
187
187
  }
188
+ function sentToOf(result) {
189
+ return {
190
+ ...(result.sentTo && typeof result.sentTo === 'object' ? { sentTo: result.sentTo } : {}),
191
+ ...(result.approveIn && typeof result.approveIn === 'object' ? { approveIn: result.approveIn } : {}),
192
+ ...(typeof result.message === 'string' ? { message: result.message } : {}),
193
+ };
194
+ }
@@ -1,3 +1,4 @@
1
+ import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
1
2
  // one shaping rule for every agreement this package parses.
2
3
  import { shapeAgreement } from './AgreementClient.js';
3
4
  import { getBackendUrl } from '../utils/urlUtils.js';
@@ -9,7 +10,7 @@ const buildHeaders = buildCredsHeaders;
9
10
  function assertCreds(creds, op) {
10
11
  if (!creds?.operatorKey)
11
12
  throw new Error(`operatorKey is required for ${op}`);
12
- if (!creds?.agentId)
13
+ if (!creds?.agentId && !decodeOperatorKeyClaims(creds.operatorKey)?.connectionClientId)
13
14
  throw new Error(`agentId is required for ${op}`);
14
15
  }
15
16
  export async function publishOffer(payload, creds) {
@@ -1,3 +1,5 @@
1
+ import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
2
+ import { buildOperatorHeaders } from './operatorHeaders.js';
1
3
  import { getBackendUrl } from '../utils/urlUtils.js';
2
4
  import { throwApiError } from '../shared/apiError.js';
3
5
  /**
@@ -14,7 +16,7 @@ export class MessagesClient {
14
16
  constructor(operatorKey, agentId) {
15
17
  if (!operatorKey)
16
18
  throw new Error('MessagesClient: operatorKey is required');
17
- if (!agentId)
19
+ if (!agentId && !decodeOperatorKeyClaims(operatorKey)?.connectionClientId)
18
20
  throw new Error('MessagesClient: agentId is required (operator-token impersonation)');
19
21
  this.operatorKey = operatorKey;
20
22
  this.agentId = agentId;
@@ -43,8 +45,7 @@ export class MessagesClient {
43
45
  _headers() {
44
46
  return {
45
47
  'content-type': 'application/json',
46
- Authorization: `Bearer ${this.operatorKey}`,
47
- 'X-Agent-Id': this.agentId,
48
+ ...buildOperatorHeaders(this.operatorKey, this.agentId),
48
49
  };
49
50
  }
50
51
  }
@@ -102,7 +102,6 @@ export async function fetchHostedAgentAccess(creds, baseUrl) {
102
102
  orgId: activeOrgId,
103
103
  orgName,
104
104
  orgKind,
105
- switchOrgHint: 'Hosted agent session — acting org is the agent home / key workspace, not MCP OAuth consent.',
106
105
  };
107
106
  }
108
107
  /**
@@ -1,3 +1,4 @@
1
+ import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
1
2
  import { getBackendUrl } from '../utils/urlUtils.js';
2
3
  import { partyActorIds } from '../types.js';
3
4
  import { throwApiError } from '../shared/apiError.js';
@@ -8,7 +9,7 @@ const buildHeaders = buildCredsHeaders;
8
9
  function assertCreds(creds, op) {
9
10
  if (!creds?.operatorKey)
10
11
  throw new Error(`operatorKey is required for ${op}`);
11
- if (!creds?.agentId)
12
+ if (!creds?.agentId && !decodeOperatorKeyClaims(creds.operatorKey)?.connectionClientId)
12
13
  throw new Error(`agentId is required for ${op}`);
13
14
  }
14
15
  function extractTask(data) {
@@ -0,0 +1,25 @@
1
+ import type { Creds } from '../types.js';
2
+ /** One catalog row as the picker reads it. */
3
+ export interface ToolPickOption {
4
+ name: string;
5
+ line: string;
6
+ }
7
+ /**
8
+ * - `pick` one tool does the need: `tool` names it.
9
+ * - `nothing_fits` no tool does it.
10
+ * - `unsure` / `off` no answer worth giving: show the list.
11
+ */
12
+ export interface ToolPickAnswer {
13
+ how: 'pick' | 'nothing_fits' | 'unsure' | 'off';
14
+ tool?: string;
15
+ confidence?: number;
16
+ }
17
+ /** Null means "no answer": the surface shows the list it always has. */
18
+ export type PickTool = (need: string, options: ToolPickOption[]) => Promise<ToolPickAnswer | null>;
19
+ /**
20
+ * The backend picker: `POST /internal/tool-pick` with this connection's key.
21
+ * The model call and its key live on the backend, since this server also runs
22
+ * on a caller's own machine. Any failure is null, including the 404 from a
23
+ * backend that predates the route.
24
+ */
25
+ export declare function backendToolPick(creds: Creds): PickTool;
@@ -0,0 +1,35 @@
1
+ import { buildCredsHeaders } from './operatorHeaders.js';
2
+ import { getBackendUrl } from '../utils/urlUtils.js';
3
+ /** A listing is always the fallback, so waiting long for a pick buys nothing. */
4
+ const TIMEOUT_MS = 6000;
5
+ /**
6
+ * The backend picker: `POST /internal/tool-pick` with this connection's key.
7
+ * The model call and its key live on the backend, since this server also runs
8
+ * on a caller's own machine. Any failure is null, including the 404 from a
9
+ * backend that predates the route.
10
+ */
11
+ export function backendToolPick(creds) {
12
+ return async (need, options) => {
13
+ if (!creds.operatorKey)
14
+ return null;
15
+ try {
16
+ const res = await fetch(`${getBackendUrl()}/internal/tool-pick`, {
17
+ method: 'POST',
18
+ headers: buildCredsHeaders(creds),
19
+ body: JSON.stringify({ need, options }),
20
+ signal: AbortSignal.timeout(TIMEOUT_MS),
21
+ });
22
+ if (!res.ok) {
23
+ await res.body?.cancel().catch(() => undefined);
24
+ return null;
25
+ }
26
+ const data = (await res.json().catch(() => null));
27
+ if (!data || typeof data.how !== 'string')
28
+ return null;
29
+ return data;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ };
35
+ }
@@ -33,3 +33,4 @@ export { InboxClient } from './InboxClient.js';
33
33
  export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from '../inboxAck.js';
34
34
  export type { InboxAckDecision, InboxAckStep, InboxAckEnvelope, PlanInboxAckOptions, } from '../inboxAck.js';
35
35
  export { WAKE_LANE_HEADER, laneHeader, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
36
+ export { backendToolPick, type ToolPickOption, type ToolPickAnswer, type PickTool } from './ToolPickClient.js';
@@ -25,3 +25,4 @@ export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds,
25
25
  // because the MCP transports in agent-sdk and ziggs-mcp send it too, and a
26
26
  // copy of a header nobody notices missing goes stale unseen.
27
27
  export { WAKE_LANE_HEADER, laneHeader, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
28
+ export { backendToolPick } from './ToolPickClient.js';
@@ -1,3 +1,10 @@
1
+ type AssistantHints = {
2
+ orgId?: string;
3
+ resource?: string;
4
+ };
5
+ /** Node-only, opt-in request context. Ordinary HTTP clients remain browser-safe. */
6
+ export declare function withAssistantRequest<T>(hints: AssistantHints, run: () => T): Promise<Awaited<T>>;
7
+ export declare function assistantRequestHeaders(): Record<string, string>;
1
8
  /**
2
9
  * Auth headers for operator-key HTTP clients.
3
10
  *
@@ -29,3 +36,4 @@ export declare function buildCredsHeaders(creds: {
29
36
  agentId: string;
30
37
  laneId?: string;
31
38
  }): Record<string, string>;
39
+ export {};
@@ -1,3 +1,17 @@
1
+ let assistantRequest;
2
+ let assistantContextLoading;
3
+ /** Node-only, opt-in request context. Ordinary HTTP clients remain browser-safe. */
4
+ export async function withAssistantRequest(hints, run) {
5
+ assistantContextLoading ??= import('node:async_hooks').then(({ AsyncLocalStorage }) => {
6
+ assistantRequest = new AsyncLocalStorage();
7
+ return assistantRequest;
8
+ });
9
+ return await (await assistantContextLoading).run(hints, run);
10
+ }
11
+ export function assistantRequestHeaders() {
12
+ const hints = assistantRequest?.getStore();
13
+ return { ...(hints?.orgId ? { 'X-Workspace-Id': hints.orgId } : {}), ...(hints?.resource ? { 'X-Ziggs-Resource': hints.resource } : {}) };
14
+ }
1
15
  /**
2
16
  * Auth headers for operator-key HTTP clients.
3
17
  *
@@ -27,6 +41,7 @@ export function buildOperatorHeaders(operatorKey, agentId, extra,
27
41
  laneId) {
28
42
  return {
29
43
  Authorization: `Bearer ${operatorKey}`,
44
+ ...assistantRequestHeaders(),
30
45
  // `X-Agent-Id` is only sent when an agentId is present — agent-scoped keys
31
46
  // identify the agent themselves; fleet keys must pass one.
32
47
  ...(agentId ? { 'X-Agent-Id': agentId } : {}),
@@ -34,12 +34,13 @@ export interface PlanInboxAckOptions {
34
34
  * delivery counts (the MCP tool with no configured self id).
35
35
  */
36
36
  ownAgentId?: string;
37
+ ownAgentIds?: readonly string[];
37
38
  /** Failed handovers hold the watermark unless `forceAck`. */
38
39
  failures?: number;
39
40
  forceAck?: boolean;
40
41
  }
41
42
  /** Resource ids this envelope's ack must list. */
42
- export declare function inboxAckHandledIds(envelope: InboxAckEnvelope, ownAgentId?: string): string[];
43
+ export declare function inboxAckHandledIds(envelope: InboxAckEnvelope, ownAgentId?: string, ownAgentIds?: readonly string[]): string[];
43
44
  /** Whether an ack is allowed for this envelope, given handover state. */
44
45
  export declare function inboxAckAllowed(envelope: InboxAckEnvelope, opts?: PlanInboxAckOptions): boolean;
45
46
  export declare function planInboxAck(envelope: InboxAckEnvelope, opts?: PlanInboxAckOptions): InboxAckDecision;
package/dist/inboxAck.js CHANGED
@@ -15,8 +15,8 @@ function isOutOfReach(note) {
15
15
  typeof n.remedy === 'string');
16
16
  }
17
17
  /** Resource ids this envelope's ack must list. */
18
- export function inboxAckHandledIds(envelope, ownAgentId) {
19
- const mine = (assigneeId) => !ownAgentId || assigneeId === ownAgentId;
18
+ export function inboxAckHandledIds(envelope, ownAgentId, ownAgentIds) {
19
+ const mine = (assigneeId) => ownAgentIds ? ownAgentIds.includes(assigneeId ?? '') : !ownAgentId || assigneeId === ownAgentId;
20
20
  return [
21
21
  ...new Set([
22
22
  ...(envelope.deliveries ?? [])
@@ -41,12 +41,12 @@ export function inboxAckAllowed(envelope, opts = {}) {
41
41
  export function planInboxAck(envelope, opts = {}) {
42
42
  return {
43
43
  allowed: inboxAckAllowed(envelope, opts),
44
- handledResourceIds: inboxAckHandledIds(envelope, opts.ownAgentId),
44
+ handledResourceIds: inboxAckHandledIds(envelope, opts.ownAgentId, opts.ownAgentIds),
45
45
  };
46
46
  }
47
47
  /** Every row whose instant the server's bury-guard will ask about. */
48
- function gatingRows(envelope, ownAgentId) {
49
- const mine = (assigneeId) => !ownAgentId || assigneeId === ownAgentId;
48
+ function gatingRows(envelope, ownAgentId, ownAgentIds) {
49
+ const mine = (assigneeId) => ownAgentIds ? ownAgentIds.includes(assigneeId ?? '') : !ownAgentId || assigneeId === ownAgentId;
50
50
  return [
51
51
  ...(envelope.deliveries ?? [])
52
52
  .filter((d) => mine(d.assigneeId))
@@ -92,7 +92,7 @@ export function planPartialInboxAck(envelope, handled, opts = {}) {
92
92
  }
93
93
  if (tokenByInstant.size === 0)
94
94
  return null;
95
- const gating = gatingRows(envelope, opts.ownAgentId);
95
+ const gating = gatingRows(envelope, opts.ownAgentId, opts.ownAgentIds);
96
96
  // ISO-8601 sorts lexicographically in time order, which is what every other
97
97
  // ts comparison on this rail already relies on.
98
98
  const instants = [...tokenByInstant.keys()].sort();
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiE
12
12
  export { ApiError } from './types.js';
13
13
  export type { Creds, Task, TaskState, PlanStep, PlanStepStatus, Agreement, AgreementParties, AgreementPartySide, EngagementKind, BroadcastAudience, EntryType, ContentType, MessageMetadata, MessageHandler, InboxDeliveryKind, InboxDeliveryRef, InboxChatNews, InboxProposalRef, InboxTaskRef, InboxRequestRef, InboxConnectionRequestRef, InboxHumanAttention, InboxEnvelope, InboxAckResult, InboxPeek, InboxReadOptions, } from './types.js';
14
14
  export type { ProposeTerms, ProposeDirectInput, ProposeBroadcastInput, ProposeAgreementData, ClaimOptions, ClaimRoom, } from './http/AgreementClient.js';
15
- export { decisionWords } from './decisionWords.js';
15
+ export { decisionWords, speakableActor } from './decisionWords.js';
16
16
  export type { DecisionOutcome, DecisionWords, DecisionWordsInput, } from './decisionWords.js';
17
17
  export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from './inboxAck.js';
18
18
  export type { InboxAckDecision, InboxAckStep, InboxAckEnvelope, PlanInboxAckOptions, } from './inboxAck.js';
@@ -24,3 +24,6 @@ export { sessionOrientation, } from './sessionOrientation.js';
24
24
  export type { ContinuationKind, InboxAckDriver, InboxHandling, RepresentedPerson, SessionContinuation, SessionOrientation, SessionOrientationInput, } from './sessionOrientation.js';
25
25
  export { inboxEngagement, isEchoOrDuplicate, isHireWorkOrder, contactPath, formationGuide, marketplaceFormation, liveHireChangedTerms, liveHireConflictFromStatus, withdrawHeldGraph, reminderHonesty, graphReleaseHonesty, hintsFromTasks, } from './engagementGuide.js';
26
26
  export type { EngagementGuide, EngagementGuideKind, EngagementOutcome, EngagementTaskHint, ContactPathInput, FormationInput, GraphHonesty, HireTermsHonesty, WaitingOn, } from './engagementGuide.js';
27
+ export { isContextAccessRequest } from './shared/contextAccessRequest.js';
28
+ export { AssistantConnectionClient, type AssistantWorkspace, } from './http/AssistantConnectionClient.js';
29
+ export { withAssistantRequest, assistantRequestHeaders } from './http/operatorHeaders.js';
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiE
15
15
  export { ApiError } from './types.js';
16
16
  // One decision, one sentence, on every rail: the reason a delivery row carries
17
17
  // when somebody answered a request this agent made, rendered as words.
18
- export { decisionWords } from './decisionWords.js';
18
+ export { decisionWords, speakableActor } from './decisionWords.js';
19
19
  export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from './inboxAck.js';
20
20
  export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId } from '@ziggs-ai/contracts';
21
21
  export * from './shared/operatorKey.js';
@@ -23,3 +23,6 @@ export { INBOX_HOST_CLAIMANT_HEADER, INSTANCE_IDENTITY_MAX_LENGTH, cliHostIdenti
23
23
  export * from './utils/appUrls.js';
24
24
  export { sessionOrientation, } from './sessionOrientation.js';
25
25
  export { inboxEngagement, isEchoOrDuplicate, isHireWorkOrder, contactPath, formationGuide, marketplaceFormation, liveHireChangedTerms, liveHireConflictFromStatus, withdrawHeldGraph, reminderHonesty, graphReleaseHonesty, hintsFromTasks, } from './engagementGuide.js';
26
+ export { isContextAccessRequest } from './shared/contextAccessRequest.js';
27
+ export { AssistantConnectionClient, } from './http/AssistantConnectionClient.js';
28
+ export { withAssistantRequest, assistantRequestHeaders } from './http/operatorHeaders.js';
@@ -0,0 +1,12 @@
1
+ /** The common marker carried by access-request approvals and notifications. */
2
+ export interface ContextAccessRequest {
3
+ primitive: 'context';
4
+ kind: 'access-request';
5
+ holderId?: string;
6
+ scope?: {
7
+ kind?: string;
8
+ id?: string;
9
+ };
10
+ resumeAgreementId?: string;
11
+ }
12
+ export declare function isContextAccessRequest(value: unknown): value is ContextAccessRequest;
@@ -0,0 +1,6 @@
1
+ export function isContextAccessRequest(value) {
2
+ if (!value || typeof value !== 'object')
3
+ return false;
4
+ const row = value;
5
+ return row.primitive === 'context' && row.kind === 'access-request';
6
+ }
@@ -5,6 +5,7 @@ export interface OperatorKeyClaims {
5
5
  keyId?: string;
6
6
  ownerId?: string;
7
7
  boundAgentId?: string | null;
8
+ connectionClientId?: string | null;
8
9
  /**
9
10
  * How the key was authorized. MCP consent uses `mcp_oauth` or `device_code`;
10
11
  * provisioning and other mint paths carry their own provenance.
@@ -14,6 +14,7 @@ export function decodeOperatorKeyClaims(token) {
14
14
  keyId: payload.keyId,
15
15
  ownerId: payload.ownerId,
16
16
  boundAgentId: typeof payload.boundAgentId === 'string' ? payload.boundAgentId : null,
17
+ connectionClientId: typeof payload.connectionClientId === 'string' ? payload.connectionClientId : null,
17
18
  issuedVia: typeof payload.issuedVia === 'string' ? payload.issuedVia : undefined,
18
19
  exp: payload.exp,
19
20
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.30.3",
3
+ "version": "0.32.0",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",