@ziggs-ai/api-client 0.31.0 → 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.
@@ -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
  }
@@ -369,13 +369,13 @@ export const contextRequestCapability = {
369
369
  title: 'Ask an owner for artifact access',
370
370
  descriptions: {
371
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.',
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 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.',
373
373
  },
374
374
  annotation: 'write', needsAgentId: true,
375
375
  params: {
376
376
  artifactId: { type: 'string', required: true },
377
377
  chatId: { type: 'string', required: true },
378
- 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.' },
379
379
  reason: { type: 'string', required: true, description: 'Purpose, including whom you will summarize the artifact to' },
380
380
  durationHours: { type: 'number', description: 'Integer 1–168 hours after approval; default 24' },
381
381
  idempotencyKey: { type: 'string', description: 'Reuse for the same request; do not create duplicate approval cards' },
@@ -391,7 +391,7 @@ export const contextRequestCapability = {
391
391
  const result = await new ContextGrantsClient(creds.operatorKey, creds.agentId, env.baseUrl).requestArtifactAccess(args.artifactId, {
392
392
  chatId: args.chatId, ownerId: args.ownerId, reason: args.reason, durationHours,
393
393
  }, { idempotencyKey: args.idempotencyKey, laneId: creds.laneId });
394
- 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.` };
395
395
  },
396
396
  };
397
397
  /** A suggested request still needs a purpose; it is never a grant or an approval. */
@@ -399,7 +399,7 @@ function requestAccessAction(args, env) {
399
399
  return {
400
400
  kind: 'request_access',
401
401
  tool: env.surface === 'mcp' ? 'ziggs_access_request' : 'access_request',
402
- 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',
403
403
  requiresApproval: true,
404
404
  };
405
405
  }
@@ -409,8 +409,8 @@ export const accessRequestCapability = {
409
409
  title: 'Request access for yourself',
410
410
  annotation: 'write', needsAgentId: true,
411
411
  descriptions: {
412
- sdk: 'Request optional bounded access to one artifact, chat, agreement context or task. Creates only a pending approval; never grants access or approves. Artifact requests need chatId and ownerId of the person authorized to share it. 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. Artifact requests require chatId and ownerId of the authorized person in that conversation. 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
+ 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.',
414
414
  },
415
415
  params: {
416
416
  scopeKind: { type: 'string', required: true, enum: ['artifact', 'chat', 'agreement', 'task'] },
@@ -421,11 +421,11 @@ export const accessRequestCapability = {
421
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.' },
422
422
  durationHours: { type: 'number', description: 'Integer 1–168 hours after approval; default 24' },
423
423
  chatId: { type: 'string', description: 'Artifact or workspace requests only: existing conversation with the owner' },
424
- ownerId: { type: 'string', description: 'Artifact or workspace requests only: human account or participant reference in that conversation' },
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)' },
425
425
  idempotencyKey: { type: 'string', description: 'Reuse for the same logical request' },
426
426
  },
427
427
  handler: async (args, env) => {
428
- for (const key of ['scopeKind', 'reason']) {
428
+ for (const key of ['scopeKind', 'reason', 'recipientId']) {
429
429
  if (typeof args[key] !== 'string' || !String(args[key]).trim())
430
430
  throw new Error(`${key} is required`);
431
431
  }
@@ -448,14 +448,15 @@ export const accessRequestCapability = {
448
448
  const result = await new ContextGrantsClient(creds.operatorKey, creds.agentId, env.baseUrl).requestAccess({
449
449
  scopeKind, scopeId, agreementId, reason: String(args.reason).trim(),
450
450
  permission: permission, temporal: args.temporal,
451
- durationHours, chatId: args.chatId, ownerId: args.ownerId,
451
+ durationHours, chatId: args.chatId,
452
+ recipientId: typeof args.recipientId === 'string' ? args.recipientId.trim() : undefined,
452
453
  }, { idempotencyKey: args.idempotencyKey, laneId: creds.laneId });
453
454
  return {
454
455
  ...result, outcome: 'pending', state: 'requested', actingAgentId: creds.agentId,
455
456
  scope: result.scope ?? { kind: scopeKind, id: scopeId },
456
457
  appUrl: `${(env.webUrl ?? 'https://ziggsai.com').replace(/\/$/, '')}/app/agreements/${encodeURIComponent(result.agreementId)}`,
457
458
  resumeAfterApproval: { tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: result.scope ? { [`${result.scope.kind}Id`]: result.scope.id } : { [`${scopeKind}Id`]: scopeId } },
458
- note: 'No access granted. The authorized person must approve; then open the original resource using the same agent identity.',
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.`,
459
460
  };
460
461
  },
461
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
  }
@@ -107,6 +107,8 @@ export declare class ContextGrantsClient {
107
107
  durationHours?: number;
108
108
  chatId?: string;
109
109
  ownerId?: string;
110
+ /** Who to ask, from the people the server offers this wake. */
111
+ recipientId?: string;
110
112
  }, opts?: {
111
113
  idempotencyKey?: string;
112
114
  laneId?: string;
@@ -118,7 +120,7 @@ export declare class ContextGrantsClient {
118
120
  kind: string;
119
121
  id: string;
120
122
  };
121
- }>;
123
+ } & RequestSentTo>;
122
124
  requestArtifactAccess(artifactId: string, input: {
123
125
  chatId: string;
124
126
  ownerId: string;
@@ -130,7 +132,7 @@ export declare class ContextGrantsClient {
130
132
  }): Promise<{
131
133
  status: 'pending_approval';
132
134
  agreementId: string;
133
- }>;
135
+ } & RequestSentTo>;
134
136
  delegateGrant(parentGrantId: string, input: DelegateContextGrantInput, opts?: {
135
137
  idempotencyKey?: string;
136
138
  }): Promise<DelegateContextGrantResult>;
@@ -163,3 +165,19 @@ export declare class ContextGrantsClient {
163
165
  revokedCount?: number;
164
166
  }>;
165
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, ...(result.scope ? { scope: result.scope } : {}) };
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) {
@@ -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';
@@ -25,3 +25,5 @@ export type { ContinuationKind, InboxAckDriver, InboxHandling, RepresentedPerson
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
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';
@@ -24,3 +24,5 @@ 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
26
  export { isContextAccessRequest } from './shared/contextAccessRequest.js';
27
+ export { AssistantConnectionClient, } from './http/AssistantConnectionClient.js';
28
+ export { withAssistantRequest, assistantRequestHeaders } from './http/operatorHeaders.js';
@@ -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.31.0",
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",