@ziggs-ai/ziggs-mcp 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -253,9 +253,10 @@ self = { agentId: '' }) {
253
253
  // ZIG-659: the inbox reports session-start counts and points to
254
254
  // ziggs_pending_decisions for the sessionChatCard — it no longer re-emits the
255
255
  // cards, so a session start doesn't ship the same card ~6× across tools.
256
+ // ZIG-1120: omit `activeTasks` so counts come from inbox.tasksAwaitingMe —
257
+ // callers that still pass an array (or []) keep the listTasks-derived path.
256
258
  const pending = formatPendingDecisionsPayload(inbox, origin, self, {
257
- activeTasks,
258
- activeTasksError,
259
+ ...(activeTasks !== undefined ? { activeTasks, activeTasksError } : {}),
259
260
  withSessionCard: false,
260
261
  });
261
262
  const pendingTail = pending.hasActionable === true
@@ -1,4 +1,4 @@
1
- import { type InboxEnvelope, type Task } from '@ziggs-ai/api-client';
1
+ import { type InboxEnvelope, type InboxTaskRef, type Task } from '@ziggs-ai/api-client';
2
2
  /**
3
3
  * The two ids this delegate answers for, in the order authority is checked:
4
4
  * its own agent id (what the credential impersonates, and the only slot it can
@@ -26,17 +26,23 @@ export interface PendingDecisionItem {
26
26
  proposedAt: string | null;
27
27
  proposedAtLabel: string | null;
28
28
  appUrl: string;
29
- respondableBy: DecisionResponder;
30
29
  /**
31
- * The MCP tool call the agent runs on approval null when the decision is
32
- * the human's, because offering a tool that always 403s is what sent agents
33
- * into retry loops.
30
+ * ZIG-1171 / ZIG-1087 the one bit that used to be five parallel fields
31
+ * (`respondableBy` + four null/non-null tool/say strings). Card and next-
32
+ * action copy derive the agent cues from this alone.
33
+ *
34
+ * `agent` — the pending slot is this delegate's own; ziggs_agreement_respond works.
35
+ * `human` — the slot belongs to the principal; no tool on this surface can give it.
34
36
  */
35
- respondApprove: string | null;
36
- respondReject: string | null;
37
- sayApprove: string | null;
38
- sayReject: string | null;
37
+ respondableBy: DecisionResponder;
39
38
  }
39
+ /** Chat/tool cues when {@link PendingDecisionItem.respondableBy} is `agent`. */
40
+ export declare function decisionRespondCues(item: PendingDecisionItem): {
41
+ sayApprove: string;
42
+ sayReject: string;
43
+ toolApprove: string;
44
+ toolReject: string;
45
+ } | null;
40
46
  export interface ActiveWorkItem {
41
47
  taskId: string;
42
48
  agreementId: string | null;
@@ -99,6 +105,13 @@ export declare function buildPaymentApprovalItems(approvals: Array<Record<string
99
105
  */
100
106
  export declare function filterTasksForDelegate(tasks: Task[], selfIds: ReadonlySet<string>): Task[];
101
107
  export declare function buildActiveWorkItems(tasks: Task[], webOrigin: string): ActiveWorkItem[];
108
+ /**
109
+ * ZIG-1120: inbox already carries `tasksAwaitingMe` — the open-task work
110
+ * channel. Use that for counts/cards when the caller did not round-trip
111
+ * `listTasks` (ziggs_inbox). Plan progress is absent on the ref; pending_decisions
112
+ * still fetches full Task rows when it needs the richer card.
113
+ */
114
+ export declare function buildActiveWorkItemsFromInbox(refs: InboxTaskRef[], webOrigin: string): ActiveWorkItem[];
102
115
  /**
103
116
  * Combined card: approve/reject + active tasks (what humans actually need at
104
117
  * session start). ZIG-659: this is the ONE card shipped per session start —
@@ -113,6 +126,11 @@ export declare function buildSessionChatCard(decisions: PendingDecisionItem[], w
113
126
  }): string;
114
127
  /** Structured session payload for MCP tools (ZIG-625 + active work). */
115
128
  export declare function formatPendingDecisionsPayload(inbox: InboxEnvelope, webOrigin: string, self: DecisionSelfIds, opts?: {
129
+ /**
130
+ * Full Task rows from `listTasks`. Omit (undefined) to derive active work
131
+ * from `inbox.tasksAwaitingMe` instead (ZIG-1120 — ziggs_inbox). Pass `[]`
132
+ * when the list fetch ran and returned nothing.
133
+ */
116
134
  activeTasks?: Task[];
117
135
  activeTasksError?: string;
118
136
  /**
@@ -1,4 +1,17 @@
1
1
  import { resolvePendingApprovalPartyId, } from '@ziggs-ai/api-client';
2
+ /** Chat/tool cues when {@link PendingDecisionItem.respondableBy} is `agent`. */
3
+ export function decisionRespondCues(item) {
4
+ if (item.respondableBy !== 'agent')
5
+ return null;
6
+ const id = item.agreementId;
7
+ const link = item.kind === 'link_request';
8
+ return {
9
+ sayApprove: link ? `approve link ${id}` : `approve ${id}`,
10
+ sayReject: link ? `reject link ${id}` : `reject ${id}`,
11
+ toolApprove: `ziggs_agreement_respond agreementId=${id} action=approve`,
12
+ toolReject: `ziggs_agreement_respond agreementId=${id} action=reject`,
13
+ };
14
+ }
2
15
  const TITLE_MAX = 72;
3
16
  const ACTIVE_TASK_LIMIT = 20;
4
17
  /**
@@ -58,26 +71,15 @@ function planProgress(plan) {
58
71
  const done = steps.filter((s) => s.status === 'completed' || s.status === 'skipped').length;
59
72
  return { done, total };
60
73
  }
61
- function proposalToItem(p, origin, self) {
62
- const id = p.agreementId;
63
- // The inbox lists proposals awaiting EITHER of our ids, so ask which one.
74
+ /** Whose pending slot is this — agent may answer only when the slot is its own. */
75
+ function respondableByFor(facts, self) {
76
+ // The inbox lists decisions awaiting EITHER of our ids, so ask which one.
64
77
  // Agent id first: when both owe a decision, ours is the one we can act on.
65
- const slot = resolvePendingApprovalPartyId({ pendingPartyIds: p.pendingApprovalPartyIds ?? [], proposedTo: p.proposedTo }, [self.agentId, self.ownerUserId]);
66
- const mine = slot != null && slot === self.agentId;
67
- return {
68
- kind: 'proposal',
69
- agreementId: id,
70
- title: displayTitle(p.title?.trim() || '(untitled proposal)'),
71
- subtitle: null,
72
- proposedAt: p.proposedAt,
73
- proposedAtLabel: formatWhen(p.proposedAt),
74
- appUrl: agreementAppUrl(origin, id),
75
- respondableBy: mine ? 'agent' : 'human',
76
- respondApprove: mine ? `ziggs_agreement_respond agreementId=${id} action=approve` : null,
77
- respondReject: mine ? `ziggs_agreement_respond agreementId=${id} action=reject` : null,
78
- sayApprove: mine ? `approve ${id}` : null,
79
- sayReject: mine ? `reject ${id}` : null,
80
- };
78
+ const slot = resolvePendingApprovalPartyId({
79
+ pendingPartyIds: facts.pendingApprovalPartyIds ?? [],
80
+ proposedTo: facts.proposedTo,
81
+ }, [self.agentId, self.ownerUserId]);
82
+ return slot != null && slot === self.agentId ? 'agent' : 'human';
81
83
  }
82
84
  function linkRequesterHeadline(c) {
83
85
  // ZIG-1039: consent cards lead with a readable name + org; ZIG-1137: ref is
@@ -90,15 +92,34 @@ function linkRequesterHeadline(c) {
90
92
  return name;
91
93
  return c.requesterRef;
92
94
  }
95
+ /** ZIG-1171 — one builder for proposal / link_request decision items. */
96
+ function decisionToItem(kind, id, fields, origin, self) {
97
+ return {
98
+ kind,
99
+ agreementId: id,
100
+ title: fields.title,
101
+ subtitle: fields.subtitle,
102
+ proposedAt: fields.proposedAt,
103
+ proposedAtLabel: formatWhen(fields.proposedAt),
104
+ appUrl: agreementAppUrl(origin, id),
105
+ respondableBy: respondableByFor(fields.facts, self),
106
+ };
107
+ }
108
+ function proposalToItem(p, origin, self) {
109
+ return decisionToItem('proposal', p.agreementId, {
110
+ title: displayTitle(p.title?.trim() || '(untitled proposal)'),
111
+ subtitle: null,
112
+ proposedAt: p.proposedAt,
113
+ facts: {
114
+ pendingApprovalPartyIds: p.pendingApprovalPartyIds,
115
+ proposedTo: p.proposedTo,
116
+ },
117
+ }, origin, self);
118
+ }
93
119
  function linkToItem(c, origin, self) {
94
- const id = c.requestId;
95
120
  const note = c.message?.trim() || null;
96
121
  const headline = linkRequesterHeadline(c);
97
- const slot = resolvePendingApprovalPartyId({ pendingPartyIds: c.pendingApprovalPartyIds ?? [], proposedTo: c.proposedTo }, [self.agentId, self.ownerUserId]);
98
- const mine = slot != null && slot === self.agentId;
99
- return {
100
- kind: 'link_request',
101
- agreementId: id,
122
+ return decisionToItem('link_request', c.requestId, {
102
123
  title: truncateText(`Agent link · ${headline}`),
103
124
  subtitle: note
104
125
  ? truncateText(note, 96)
@@ -106,14 +127,11 @@ function linkToItem(c, origin, self) {
106
127
  ? truncateText(`ref: ${c.requesterRef}`, 96)
107
128
  : null,
108
129
  proposedAt: c.requestedAt,
109
- proposedAtLabel: formatWhen(c.requestedAt),
110
- appUrl: agreementAppUrl(origin, id),
111
- respondableBy: mine ? 'agent' : 'human',
112
- respondApprove: mine ? `ziggs_agreement_respond agreementId=${id} action=approve` : null,
113
- respondReject: mine ? `ziggs_agreement_respond agreementId=${id} action=reject` : null,
114
- sayApprove: mine ? `approve link ${id}` : null,
115
- sayReject: mine ? `reject link ${id}` : null,
116
- };
130
+ facts: {
131
+ pendingApprovalPartyIds: c.pendingApprovalPartyIds,
132
+ proposedTo: c.proposedTo,
133
+ },
134
+ }, origin, self);
117
135
  }
118
136
  export function buildPendingDecisionItems(inbox, webOrigin, self) {
119
137
  const items = [];
@@ -191,6 +209,30 @@ export function buildActiveWorkItems(tasks, webOrigin) {
191
209
  };
192
210
  });
193
211
  }
212
+ /**
213
+ * ZIG-1120: inbox already carries `tasksAwaitingMe` — the open-task work
214
+ * channel. Use that for counts/cards when the caller did not round-trip
215
+ * `listTasks` (ziggs_inbox). Plan progress is absent on the ref; pending_decisions
216
+ * still fetches full Task rows when it needs the richer card.
217
+ */
218
+ export function buildActiveWorkItemsFromInbox(refs, webOrigin) {
219
+ return refs
220
+ .filter((t) => !t.state || t.state === 'active')
221
+ .slice(0, ACTIVE_TASK_LIMIT)
222
+ .map((t) => {
223
+ const agreementId = t.agreementId?.trim() || null;
224
+ return {
225
+ taskId: t.taskId,
226
+ agreementId,
227
+ title: displayTitle(t.title?.trim() || '(untitled task)'),
228
+ state: t.state || 'active',
229
+ planDone: 0,
230
+ planTotal: 0,
231
+ appUrl: agreementId ? agreementAppUrl(webOrigin, agreementId) : null,
232
+ sayWork: `work on ${t.taskId}`,
233
+ };
234
+ });
235
+ }
194
236
  function kindLabel(kind) {
195
237
  if (kind === 'proposal')
196
238
  return 'Agreement proposal';
@@ -228,11 +270,12 @@ function buildDecisionSection(items, opts) {
228
270
  lines.push('');
229
271
  lines.push(`[Review in Ziggs →](${item.appUrl})`);
230
272
  lines.push('');
231
- if (item.sayApprove && item.respondApprove && item.sayReject && item.respondReject) {
273
+ const cues = decisionRespondCues(item);
274
+ if (cues) {
232
275
  lines.push('| You say in chat | What the agent runs |');
233
276
  lines.push('|:----------------|:--------------------|');
234
- lines.push(`| \`${item.sayApprove}\` | \`${item.respondApprove}\` |`);
235
- lines.push(`| \`${item.sayReject}\` | \`${item.respondReject}\` |`);
277
+ lines.push(`| \`${cues.sayApprove}\` | \`${cues.toolApprove}\` |`);
278
+ lines.push(`| \`${cues.sayReject}\` | \`${cues.toolReject}\` |`);
236
279
  }
237
280
  else {
238
281
  // ZIG-1087 — this one is bound to the human's own party slot. Saying so
@@ -374,7 +417,9 @@ export function buildSessionChatCard(decisions, work, opts) {
374
417
  export function formatPendingDecisionsPayload(inbox, webOrigin, self, opts) {
375
418
  const withSessionCard = opts?.withSessionCard !== false;
376
419
  const decisions = buildPendingDecisionItems(inbox, webOrigin, self);
377
- const activeWork = buildActiveWorkItems(opts?.activeTasks ?? [], webOrigin);
420
+ const activeWork = opts?.activeTasks !== undefined
421
+ ? buildActiveWorkItems(opts.activeTasks, webOrigin)
422
+ : buildActiveWorkItemsFromInbox(inbox.tasksAwaitingMe ?? [], webOrigin);
378
423
  const paymentApprovals = buildPaymentApprovalItems(opts?.paymentApprovals ?? [], webOrigin);
379
424
  const truncatedProposals = inbox.truncatedProposals ?? 0;
380
425
  const truncatedConnectionRequests = inbox.truncatedConnectionRequests ?? 0;
@@ -470,8 +515,9 @@ export function buildPendingNextActions(decisions, work = []) {
470
515
  actions.push('Wait for the human to say approve/reject (e.g. `approve agr-…`) — do not auto-respond.');
471
516
  }
472
517
  for (const d of decisions.slice(0, 4)) {
473
- if (d.sayApprove && d.sayReject) {
474
- actions.push(`${kindLabel(d.kind)} ${d.agreementId}: \`${d.sayApprove}\` or \`${d.sayReject}\``);
518
+ const cues = decisionRespondCues(d);
519
+ if (cues) {
520
+ actions.push(`${kindLabel(d.kind)} ${d.agreementId}: \`${cues.sayApprove}\` or \`${cues.sayReject}\``);
475
521
  }
476
522
  else {
477
523
  actions.push(`${kindLabel(d.kind)} ${d.agreementId}: waiting on the human's own approval — they decide it at ${d.appUrl}; ziggs_agreement_respond cannot.`);
@@ -1,26 +1,26 @@
1
1
  /**
2
2
  * ZIG-667 — one error surface for every MCP tool.
3
3
  *
4
- * Raw client exceptions read like
5
- * `ContextReadClient.read messages 403 {"error":"not authorized for this scope"}`
6
- * an internal class.method name, a raw HTTP status, and a raw backend body.
7
- * LLM callers stall on stack-trace prose; they recover from errors they can
8
- * parse. This module strips the internals, keeps a stable machine-readable
9
- * code, and for authorization denials says what to do next.
4
+ * ZIG-1124 classify from the thrown {@link ApiError} fields (`status`,
5
+ * `code`, `body` / `message`). No client-prefix strip, no HTTP-status regex,
6
+ * no re-parsing a JSON body that was concatenated into the message string.
10
7
  */
11
8
  export interface ToolErrorShape {
12
9
  code: string;
13
10
  message: string;
14
11
  hint?: string;
15
12
  }
16
- /** Classify a raw client error message into a stable shape. */
17
- export declare function classifyToolError(rawMessage: string, knownStatus?: number | null): ToolErrorShape;
13
+ /**
14
+ * Classify a thrown client error (or a plain string this layer raised) into a
15
+ * stable MCP shape.
16
+ */
17
+ export declare function classifyToolError(input: unknown): ToolErrorShape;
18
18
  /**
19
19
  * MCP tool error result: machine-readable code + cleaned message (+ hint).
20
20
  *
21
21
  * Pass the caught error itself, not `err.message` — an `ApiError` carries the
22
- * HTTP status that decides the code and the hint (ZIG-1086). A plain string is
23
- * still accepted for refusals this layer raises on its own.
22
+ * HTTP status and server `code` that decide the hint (ZIG-1086 / ZIG-1124).
23
+ * A plain string is still accepted for refusals this layer raises on its own.
24
24
  */
25
25
  export declare function toolError(input: unknown): {
26
26
  content: {
package/dist/toolError.js CHANGED
@@ -1,15 +1,10 @@
1
1
  /**
2
2
  * ZIG-667 — one error surface for every MCP tool.
3
3
  *
4
- * Raw client exceptions read like
5
- * `ContextReadClient.read messages 403 {"error":"not authorized for this scope"}`
6
- * an internal class.method name, a raw HTTP status, and a raw backend body.
7
- * LLM callers stall on stack-trace prose; they recover from errors they can
8
- * parse. This module strips the internals, keeps a stable machine-readable
9
- * code, and for authorization denials says what to do next.
4
+ * ZIG-1124 classify from the thrown {@link ApiError} fields (`status`,
5
+ * `code`, `body` / `message`). No client-prefix strip, no HTTP-status regex,
6
+ * no re-parsing a JSON body that was concatenated into the message string.
10
7
  */
11
- const CLIENT_PREFIX = /^[A-Z][A-Za-z0-9]*Client\.[A-Za-z0-9_]+\s+/;
12
- const HTTP_STATUS = /(?:^|\s)([1-5]\d{2})(?=\s|$)/;
13
8
  const SCOPE_DENIED_HINT = 'You are not authorized for this scope. To get access: ask the counterparty ' +
14
9
  'to issue you a context grant (they run ziggs_context_issue_grant), or propose a ' +
15
10
  'bilateral link first (ziggs_agreement_propose with engagementKind "link"). Check what you can already ' +
@@ -39,51 +34,52 @@ function codeForStatus(status) {
39
34
  return 'BAD_REQUEST';
40
35
  return 'TOOL_ERROR';
41
36
  }
42
- /** Pull a human reason out of an embedded backend JSON body, if any. */
43
- function extractBodyReason(raw) {
44
- const start = raw.indexOf('{');
45
- if (start === -1)
46
- return null;
47
- try {
48
- const parsed = JSON.parse(raw.slice(start));
49
- const reason = parsed['error'] ?? parsed['message'];
50
- return typeof reason === 'string' && reason.trim() ? reason.trim() : null;
51
- }
52
- catch {
53
- return null;
54
- }
55
- }
56
37
  /**
57
38
  * An `ApiError` from api-client, recognised by shape rather than `instanceof`:
58
39
  * a bundled or duplicated copy of the class would fail an identity check while
59
40
  * carrying exactly the fields we need.
60
41
  */
61
- function thrownStatus(err) {
62
- if (typeof err !== 'object' || err === null)
63
- return null;
64
- const status = err.status;
65
- return typeof status === 'number' && status >= 100 && status < 600 ? status : null;
42
+ function thrownFields(err) {
43
+ if (typeof err === 'string') {
44
+ return { message: err, status: null, code: null };
45
+ }
46
+ if (typeof err !== 'object' || err === null) {
47
+ return { message: String(err), status: null, code: null };
48
+ }
49
+ const o = err;
50
+ const status = typeof o.status === 'number' && o.status >= 100 && o.status < 600
51
+ ? o.status
52
+ : null;
53
+ const code = typeof o.code === 'string' && o.code ? o.code : null;
54
+ const message = typeof o.message === 'string' && o.message ? o.message : String(err);
55
+ return { message, status, code };
66
56
  }
67
- /** Classify a raw client error message into a stable shape. */
68
- export function classifyToolError(rawMessage, knownStatus) {
69
- const cleaned = rawMessage.replace(CLIENT_PREFIX, '').trim();
70
- // ZIG-1086: the status is scraped from the message only as a LAST resort.
71
- // `ApiError` carries it as a real field, and every caller that dropped it
72
- // (passing `err.message` instead of `err`) landed here with nothing to match:
73
- // "missing scope: context:admin" has no digits, so it fell through to a bare
74
- // TOOL_ERROR and the agent never saw the recovery hint it needed.
75
- const statusMatch = HTTP_STATUS.exec(cleaned);
76
- const status = knownStatus ?? (statusMatch ? Number(statusMatch[1]) : null);
77
- const bodyReason = extractBodyReason(cleaned);
57
+ /**
58
+ * Classify a thrown client error (or a plain string this layer raised) into a
59
+ * stable MCP shape.
60
+ */
61
+ export function classifyToolError(input) {
62
+ const { message, status, code: machineCode } = thrownFields(input);
63
+ if (machineCode === 'AGENT_LACKS_HUMAN_AUTHORITY') {
64
+ return {
65
+ code: 'AGENT_LACKS_HUMAN_AUTHORITY',
66
+ message,
67
+ hint: HUMAN_AUTHORITY_HINT,
68
+ };
69
+ }
70
+ if (machineCode === 'NOT_AUTHORIZED_FOR_SCOPE') {
71
+ return {
72
+ code: 'NOT_AUTHORIZED_FOR_SCOPE',
73
+ message,
74
+ hint: SCOPE_DENIED_HINT,
75
+ };
76
+ }
78
77
  if (status === null) {
79
- return { code: 'TOOL_ERROR', message: cleaned || rawMessage };
78
+ return { code: 'TOOL_ERROR', message };
80
79
  }
81
80
  const code = codeForStatus(status);
82
- // Prefer the backend's own reason over the transport framing.
83
- const message = bodyReason ?? cleaned;
84
- // The human-authority guard refuses every impersonated agent, whatever it
85
- // asked for. Checked before the scope branch below: its message also mentions
86
- // scopes, and "ask for a grant on this scope" is the wrong advice here.
81
+ // Backend human-authority already sends AGENT_LACKS_HUMAN_AUTHORITY; keep a
82
+ // message fallback for older bodies / tests that omit `code`.
87
83
  if (code === 'NOT_AUTHORIZED' && /impersonated agent cannot perform/i.test(message)) {
88
84
  return {
89
85
  code: 'AGENT_LACKS_HUMAN_AUTHORITY',
@@ -91,9 +87,8 @@ export function classifyToolError(rawMessage, knownStatus) {
91
87
  hint: HUMAN_AUTHORITY_HINT,
92
88
  };
93
89
  }
94
- // Context-scope denials (the backend says "…for this scope") get the scope
95
- // code and a recovery path. Other 403s (connection grants, party checks)
96
- // keep the generic code — their fixes live in other domains.
90
+ // Many scope denials still throw a bare ForbiddenException string with no
91
+ // machine code classify those by the word "scope" in the message.
97
92
  if (code === 'NOT_AUTHORIZED' && /\bscope\b/i.test(message)) {
98
93
  return {
99
94
  code: 'NOT_AUTHORIZED_FOR_SCOPE',
@@ -107,14 +102,11 @@ export function classifyToolError(rawMessage, knownStatus) {
107
102
  * MCP tool error result: machine-readable code + cleaned message (+ hint).
108
103
  *
109
104
  * Pass the caught error itself, not `err.message` — an `ApiError` carries the
110
- * HTTP status that decides the code and the hint (ZIG-1086). A plain string is
111
- * still accepted for refusals this layer raises on its own.
105
+ * HTTP status and server `code` that decide the hint (ZIG-1086 / ZIG-1124).
106
+ * A plain string is still accepted for refusals this layer raises on its own.
112
107
  */
113
108
  export function toolError(input) {
114
- const message = typeof input === 'string'
115
- ? input
116
- : (input?.message ?? String(input));
117
- const shape = classifyToolError(String(message), thrownStatus(input));
109
+ const shape = classifyToolError(input);
118
110
  return {
119
111
  content: [
120
112
  { type: 'text', text: JSON.stringify({ error: shape }, null, 2) },
package/dist/tools.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { z } from 'zod';
3
- import { getAgreement, getMyAgreements, listMyChats, proposeUnified, delegateAgreement, provisionRelayWorkers, respondToAgreement, revokeAgreement, counterAgreement, fulfillAgreement, sendChatMessage, ConnectionsClient, PaymentsClient, ContextReadClient, GrantsClient, InboxClient, createTask, updateTaskState, replaceTaskPlan, listTasks, getTask, getBackendUrl, fetchMyOrgs, fetchDelegateAccess, GRANTS_CAPABILITIES, CONTEXT_GRANT_SCOPE_KINDS, contextReadCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, recordArtifactCapability, listArtifactsCapability, shareArtifactCapability, attachArtifactCapability, openConversationCapability, connectionProxyCapability, requestConnectionCapability, agreementClaimCapability, marketplaceViewCapability, linkSummary, } from '@ziggs-ai/api-client';
3
+ import { getAgreement, getMyAgreements, listMyChats, proposeUnified, delegateAgreement, provisionRelayWorkers, respondToAgreement, revokeAgreement, counterAgreement, fulfillAgreement, sendChatMessage, ConnectionsClient, PaymentsClient, ContextReadClient, GrantsClient, InboxClient, createTask, updateTaskState, replaceTaskPlan, listTasks, getTask, getBackendUrl, fetchMyOrgs, fetchDelegateAccess, GRANTS_CAPABILITIES, CONTEXT_GRANT_SCOPE_KINDS, contextReadCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, recordArtifactCapability, listArtifactsCapability, shareArtifactCapability, attachArtifactCapability, openConversationCapability, connectionProxyCapability, requestConnectionCapability, agreementClaimCapability, marketplaceViewCapability, } from '@ziggs-ai/api-client';
4
4
  import { decodeOperatorKeyClaims } from './operatorKey.js';
5
5
  import { registerTrustTools } from './trustTools.js';
6
6
  import { registerPaymentTools } from './paymentTools.js';
@@ -81,27 +81,37 @@ function delegateSelfIds(creds, cfg) {
81
81
  async function loadSessionActionsPayload(creds, cfg, opts) {
82
82
  const webOrigin = resolveWebAppOrigin(cfg.ZIGGS_WEB_URL);
83
83
  const client = new InboxClient(creds.operatorKey, creds.agentId);
84
- const inbox = await client.getInbox();
84
+ // ZIG-1120 #4 — inbox / tasks / payments are independent; overlap them.
85
+ const [inboxSettled, tasksSettled, paymentsSettled] = await Promise.allSettled([
86
+ client.getInbox(),
87
+ listTasks({ state: 'active', limit: 20 }, creds),
88
+ new PaymentsClient(creds.operatorKey, creds.agentId).approvals({
89
+ status: 'pending',
90
+ }),
91
+ ]);
92
+ if (inboxSettled.status === 'rejected') {
93
+ throw inboxSettled.reason;
94
+ }
95
+ const inbox = inboxSettled.value;
85
96
  let activeTasks = [];
86
97
  let activeTasksError;
87
- try {
88
- const listed = await listTasks({ state: 'active', limit: 20 }, creds);
89
- activeTasks = filterTasksForDelegate(listed.tasks ?? [], delegateSelfIds(creds, cfg));
98
+ if (tasksSettled.status === 'fulfilled') {
99
+ activeTasks = filterTasksForDelegate(tasksSettled.value.tasks ?? [], delegateSelfIds(creds, cfg));
90
100
  }
91
- catch (e) {
101
+ else {
92
102
  // ZIG-700 — inbox is still useful when task listing fails, but surface the
93
103
  // failure so hasActiveWork:false is not mistaken for "no tasks".
94
- activeTasksError = e.message;
104
+ activeTasksError = tasksSettled.reason.message;
95
105
  }
96
106
  // ZIG-896 — paused transfers awaiting the wallet owner. Same failure rule as
97
107
  // tasks: a failed fetch is surfaced, not silently rendered as "none pending".
98
108
  let paymentApprovals = [];
99
109
  let paymentApprovalsError;
100
- try {
101
- paymentApprovals = (await new PaymentsClient(creds.operatorKey, creds.agentId).approvals({ status: 'pending' }));
110
+ if (paymentsSettled.status === 'fulfilled') {
111
+ paymentApprovals = paymentsSettled.value;
102
112
  }
103
- catch (e) {
104
- paymentApprovalsError = e.message;
113
+ else {
114
+ paymentApprovalsError = paymentsSettled.reason.message;
105
115
  }
106
116
  return formatPendingDecisionsPayload(inbox, webOrigin, {
107
117
  agentId: creds.agentId,
@@ -195,13 +205,20 @@ export function registerZiggsTools(server, creds, cfg) {
195
205
  server.tool('ziggs_auth_status', 'Verify MCP OAuth binding: delegate agent id, owner user id, and org scope. Call after connect before inbox/chats. Includes pendingDecisions summary when approve/reject is waiting. ("Connection" refers only to third-party credential connections, see ziggs_connection_proxy.)', {}, READ_ONLY, async () => {
196
206
  const claims = decodeOperatorKeyClaims(creds.operatorKey);
197
207
  const webOrigin = resolveWebAppOrigin(cfg.ZIGGS_WEB_URL);
208
+ // ZIG-1120 #4 — delegate access and session actions are independent.
209
+ const [accessSettled, pendingSettled] = await Promise.allSettled([
210
+ fetchDelegateAccess(creds),
211
+ // ZIG-659: counts + a pointer only — the full sessionChatCard is owned
212
+ // by ziggs_pending_decisions, not duplicated here.
213
+ loadSessionActionsPayload(creds, cfg, { withSessionCard: false }),
214
+ ]);
198
215
  let actingOrgId = null;
199
216
  let actingOrgName = null;
200
217
  let actingOrgKind = null;
201
218
  let connected = false;
202
219
  let switchOrgHint = null;
203
- try {
204
- const access = await fetchDelegateAccess(creds);
220
+ if (accessSettled.status === 'fulfilled') {
221
+ const access = accessSettled.value;
205
222
  actingOrgId = typeof access.orgId === 'string' ? access.orgId : null;
206
223
  actingOrgName = typeof access.orgName === 'string' ? access.orgName : null;
207
224
  actingOrgKind = typeof access.orgKind === 'string' ? access.orgKind : null;
@@ -209,8 +226,8 @@ export function registerZiggsTools(server, creds, cfg) {
209
226
  switchOrgHint =
210
227
  typeof access.switchOrgHint === 'string' ? access.switchOrgHint : null;
211
228
  }
212
- catch (e) {
213
- switchOrgHint = `Could not load runtime org: ${e.message}`;
229
+ else {
230
+ switchOrgHint = `Could not load runtime org: ${accessSettled.reason.message}`;
214
231
  }
215
232
  let pendingDecisions = {
216
233
  pendingCount: 0,
@@ -220,14 +237,10 @@ export function registerZiggsTools(server, creds, cfg) {
220
237
  actionCount: 0,
221
238
  hasActionable: false,
222
239
  };
223
- try {
224
- // ZIG-659: counts + a pointer only — the full sessionChatCard is owned
225
- // by ziggs_pending_decisions, not duplicated here.
226
- pendingDecisions = await loadSessionActionsPayload(creds, cfg, {
227
- withSessionCard: false,
228
- });
240
+ if (pendingSettled.status === 'fulfilled') {
241
+ pendingDecisions = pendingSettled.value;
229
242
  }
230
- catch {
243
+ else {
231
244
  pendingDecisions = {
232
245
  ...pendingDecisions,
233
246
  fetchError: 'Could not load inbox/tasks — call ziggs_pending_decisions.',
@@ -466,8 +479,6 @@ export function registerZiggsTools(server, creds, cfg) {
466
479
  lifecycle,
467
480
  billing,
468
481
  }, creds);
469
- // ZIG-957: link mutations return linkSummary, not the raw Mongo doc.
470
- const agreementOut = shape === 'link' && agreement ? linkSummary(agreement) : agreement;
471
482
  // ZIG-1039: surface the owner-routing rewrite so agents do not think
472
483
  // the id they passed was ignored silently.
473
484
  const routedProposedTo = shape === 'link' &&
@@ -478,7 +489,7 @@ export function registerZiggsTools(server, creds, cfg) {
478
489
  : undefined;
479
490
  return textResult({
480
491
  shape,
481
- agreement: agreementOut,
492
+ agreement,
482
493
  ...(routedProposedTo ? { note: routedProposedTo } : {}),
483
494
  ...(shape === 'quest' || shape === 'offer'
484
495
  ? {
@@ -542,9 +553,7 @@ export function registerZiggsTools(server, creds, cfg) {
542
553
  // So a refusal can name where the human goes, not just why.
543
554
  appUrl: agreementAppUrl(resolveWebAppOrigin(cfg.ZIGGS_WEB_URL), agreementId),
544
555
  });
545
- // ZIG-957: link approvals/rejects share the same summary shape as claim/revoke.
546
- const agreement = updated?.engagementKind === 'link' ? linkSummary(updated) : updated;
547
- return textResult({ agreement });
556
+ return textResult({ agreement: updated });
548
557
  }
549
558
  catch (e) {
550
559
  return toolError(e);
@@ -556,17 +565,13 @@ export function registerZiggsTools(server, creds, cfg) {
556
565
  try {
557
566
  const result = await revokeAgreement(agreementId, creds);
558
567
  const isLink = result.agreement?.engagementKind === 'link';
559
- // ZIG-957: same summary shape as create/list/claim link tools (not the raw Mongo doc).
560
- const agreement = result.agreement
561
- ? (isLink ? linkSummary(result.agreement) : result.agreement)
562
- : undefined;
563
568
  return textResult({
564
569
  status: 'revoked',
565
570
  agreementId,
566
571
  ...(isLink
567
572
  ? { note: 'Link revoked — unpublished cross-org reach to this peer is blocked again.' }
568
573
  : {}),
569
- agreement,
574
+ agreement: result.agreement,
570
575
  });
571
576
  }
572
577
  catch (e) {
@@ -625,35 +630,33 @@ export function registerZiggsTools(server, creds, cfg) {
625
630
  }, READ_ONLY, async ({ ack, waitSeconds }) => {
626
631
  try {
627
632
  const client = new InboxClient(creds.operatorKey, creds.agentId);
633
+ // Ack-before-fetch is load-bearing; everything else is independent of
634
+ // the envelope until we know whether there are deliveries to tag.
628
635
  const acked = ack ? await client.ack(ack) : null;
629
636
  const inbox = await client.getInbox(waitSeconds != null ? { waitSeconds } : {});
630
- let activeTasks = [];
631
- let activeTasksError;
632
- try {
633
- const listed = await listTasks({ state: 'active', limit: 20 }, creds);
634
- activeTasks = filterTasksForDelegate(listed.tasks ?? [], delegateSelfIds(creds, cfg));
635
- }
636
- catch (e) {
637
- // ZIG-700 — surface the failure instead of silently omitting the work card.
638
- activeTasksError = e.message;
639
- }
640
- // ZIG-635: tag each scope with the covering grant from the caller's own
641
- // held context grants. Best-effort — the inbox still works untagged.
642
- // All pages of live grants only (GET /grants also returns expired/revoked
643
- // and paginates), so a scope isn't left untagged behind the first page.
637
+ // ZIG-1120 #2+#3: activeWorkCount comes from inbox.tasksAwaitingMe
638
+ // (formatInboxToolResult omits listTasks). Grant walk is only for
639
+ // readPlan pinning and is unused when deliveries is empty — the idle
640
+ // long-poll outcome so skip it there (artifact grants grow unbounded).
644
641
  let reach = [];
645
- try {
646
- reach = await new GrantsClient(creds.operatorKey, creds.agentId).listAllGrants({
647
- // Every context scope kind a literal subset here is how the CLI
648
- // silently dropped a whole kind when `artifact` was added.
649
- scopeKind: [...CONTEXT_GRANT_SCOPE_KINDS],
650
- health: 'active',
651
- });
652
- }
653
- catch {
654
- // omit grant tags when the grants read fails
642
+ if ((inbox.deliveries?.length ?? 0) > 0) {
643
+ try {
644
+ // ZIG-635: tag each scope with the covering grant from the caller's
645
+ // own held context grants. Best-effort the inbox still works
646
+ // untagged. All pages of live grants only (GET /grants also returns
647
+ // expired/revoked and paginates).
648
+ reach = await new GrantsClient(creds.operatorKey, creds.agentId).listAllGrants({
649
+ // Every context scope kind — a literal subset here is how the CLI
650
+ // silently dropped a whole kind when `artifact` was added.
651
+ scopeKind: [...CONTEXT_GRANT_SCOPE_KINDS],
652
+ health: 'active',
653
+ });
654
+ }
655
+ catch {
656
+ // omit grant tags when the grants read fails
657
+ }
655
658
  }
656
- return textResult(formatInboxToolResult(inbox, acked, cfg.ZIGGS_WEB_URL, activeTasks, reach, activeTasksError, { agentId: creds.agentId, ownerUserId: ownerPrincipalId(creds, cfg) }));
659
+ return textResult(formatInboxToolResult(inbox, acked, cfg.ZIGGS_WEB_URL, undefined, reach, undefined, { agentId: creds.agentId, ownerUserId: ownerPrincipalId(creds, cfg) }));
657
660
  }
658
661
  catch (e) {
659
662
  return toolError(e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/ziggs-mcp",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "MCP server for Claude Code, Cursor, and other MCP hosts \u2014 act as your Ziggs delegate agent",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@modelcontextprotocol/sdk": "^1.29.0",
39
- "@ziggs-ai/api-client": "^0.9.0",
39
+ "@ziggs-ai/api-client": "^0.9.1",
40
40
  "dotenv": "^16.6.1",
41
41
  "zod": "^3.24.2"
42
42
  },