@ziggs-ai/ziggs-mcp 0.8.0 → 0.9.1

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.
@@ -65,7 +65,7 @@ export function registerCapability(server, cap, creds, opts = {}) {
65
65
  return textResult(opts.transformResult ? opts.transformResult(result, args) : result);
66
66
  }
67
67
  catch (e) {
68
- return toolError(e.message);
68
+ return toolError(e);
69
69
  }
70
70
  });
71
71
  }
@@ -1,4 +1,5 @@
1
1
  import type { GrantView, ContextReadType, InboxAckResult, InboxEnvelope, Task } from '@ziggs-ai/api-client';
2
+ import { type DecisionSelfIds } from './pendingDecisions.js';
2
3
  /**
3
4
  * ZIG-634 (Step 1): a pre-filled next call. The agent can run it verbatim
4
5
  * instead of assembling args from the ids scattered through the response.
@@ -31,7 +32,7 @@ export interface ReadPlanResult {
31
32
  * Mapping honours how reads resolve server-side: messages read only via chat,
32
33
  * artifacts via the chat, agreement, or task they landed on.
33
34
  */
34
- export declare function buildReadPlan(inbox: InboxEnvelope, grantsByScope?: Map<string, ScopeGrantTag>): ReadPlanResult;
35
+ export declare function buildReadPlan(inbox: InboxEnvelope, grantsByScope?: Map<string, ScopeGrantTag>, self?: DecisionSelfIds): ReadPlanResult;
35
36
  /**
36
37
  * ZIG-634: forward-continuation for a read_context page. Built only from fields
37
38
  * already on the page (via, hasMore/nextCursor, latestSequence) plus the grant
@@ -73,4 +74,12 @@ export declare function indexReachByScope(reach: GrantView[]): Map<string, Scope
73
74
  * each read's covering grant (ZIG-635) so the agent can present
74
75
  * X-Context-Grant-Id without a separate discover round-trip.
75
76
  */
76
- export declare function formatInboxToolResult(inbox: InboxEnvelope, ack?: InboxAckResult | null, webOrigin?: string, activeTasks?: Task[], reach?: GrantView[], activeTasksError?: string): Record<string, unknown>;
77
+ export declare function formatInboxToolResult(inbox: InboxEnvelope, ack?: InboxAckResult | null, webOrigin?: string, activeTasks?: Task[], reach?: GrantView[], activeTasksError?: string,
78
+ /**
79
+ * The caller's own ids (ZIG-1087). Optional here alone: this result carries
80
+ * the session-start COUNTS and a pointer to ziggs_pending_decisions, never
81
+ * the decision items themselves, and a count does not depend on which party
82
+ * may answer. Production callers pass it regardless — if this shape ever
83
+ * starts emitting `decisions`, they must already be marked correctly.
84
+ */
85
+ self?: DecisionSelfIds): Record<string, unknown>;
@@ -1,7 +1,22 @@
1
- import { grantCaveat } from '@ziggs-ai/api-client';
1
+ import { grantCaveat, resolvePendingApprovalPartyId } from '@ziggs-ai/api-client';
2
2
  import { formatPendingDecisionsPayload, resolveWebAppOrigin, } from './pendingDecisions.js';
3
3
  /** Keep the plan bounded; the full deliveries array still carries everything. */
4
4
  const MAX_READ_PLAN = 12;
5
+ /**
6
+ * Where to read an artifact delivery from — a total function, so there is no
7
+ * "none of the above" that silently drops the row. Container first (the parties
8
+ * who can see it there are the audience the write chose), then the artifact
9
+ * itself for a free-standing one.
10
+ */
11
+ function artifactEntry(d) {
12
+ if (d.chatId)
13
+ return ['chat', d.chatId];
14
+ if (d.agreementId)
15
+ return ['agreement', d.agreementId];
16
+ if (d.taskId)
17
+ return ['task', d.taskId];
18
+ return ['artifact', d.resourceId];
19
+ }
5
20
  function readContextCall(type, kind, id, grantId) {
6
21
  // ZIG-660: pin the covering grant so the read presents the right
7
22
  // X-Context-Grant-Id without a separate discover_context round-trip.
@@ -25,7 +40,7 @@ function readContextCall(type, kind, id, grantId) {
25
40
  * Mapping honours how reads resolve server-side: messages read only via chat,
26
41
  * artifacts via the chat, agreement, or task they landed on.
27
42
  */
28
- export function buildReadPlan(inbox, grantsByScope) {
43
+ export function buildReadPlan(inbox, grantsByScope, self = { agentId: '' }) {
29
44
  const proposals = inbox.proposalsAwaitingMe ?? [];
30
45
  const connectionRequests = inbox.connectionRequestsAwaitingMe ?? [];
31
46
  const deliveries = inbox.deliveries ?? [];
@@ -43,19 +58,40 @@ export function buildReadPlan(inbox, grantsByScope) {
43
58
  };
44
59
  // Decisions first — these also drive humanAttention (pull-only: no push).
45
60
  // The decision (approve/reject) is the human's; we only pre-fill the target.
61
+ //
62
+ // ZIG-1087: only when the pending slot is OURS. A proposal bound to the
63
+ // principal's slot cannot be answered with this tool by anyone on this
64
+ // surface, so pre-filling the call would be handing over a step that fails
65
+ // every time — the plan says to carry it to the human instead.
66
+ const mayAnswer = (facts) => resolvePendingApprovalPartyId({
67
+ pendingPartyIds: facts.pendingApprovalPartyIds ?? [],
68
+ proposedTo: facts.proposedTo,
69
+ }, [self.agentId]) === self.agentId && self.agentId !== '';
46
70
  for (const p of proposals) {
47
- add(`respond:${p.agreementId}`, {
48
- tool: 'ziggs_agreement_respond',
49
- args: { agreementId: p.agreementId },
50
- why: 'agreement proposal awaiting your response — wait for the human to approve/reject',
51
- });
71
+ add(`respond:${p.agreementId}`, mayAnswer(p)
72
+ ? {
73
+ tool: 'ziggs_agreement_respond',
74
+ args: { agreementId: p.agreementId },
75
+ why: 'agreement proposal awaiting your response — wait for the human to approve/reject',
76
+ }
77
+ : {
78
+ tool: 'ziggs_pending_decisions',
79
+ args: {},
80
+ why: `proposal ${p.agreementId} is awaiting your HUMAN's approval, not yours — paste the card for them; ziggs_agreement_respond is refused for a delegate here`,
81
+ });
52
82
  }
53
83
  for (const c of connectionRequests) {
54
- add(`respond:${c.requestId}`, {
55
- tool: 'ziggs_agreement_respond',
56
- args: { agreementId: c.requestId },
57
- why: 'connection request awaiting your response — wait for the human to approve/reject',
58
- });
84
+ add(`respond:${c.requestId}`, mayAnswer(c)
85
+ ? {
86
+ tool: 'ziggs_agreement_respond',
87
+ args: { agreementId: c.requestId },
88
+ why: 'connection request awaiting your response — wait for the human to approve/reject',
89
+ }
90
+ : {
91
+ tool: 'ziggs_pending_decisions',
92
+ args: {},
93
+ why: `connection request ${c.requestId} is awaiting your HUMAN's approval, not yours — paste the card for them; ziggs_agreement_respond is refused for a delegate here`,
94
+ });
59
95
  }
60
96
  // ZIG-635: pin the covering grant for a chat/agreement read when the caller
61
97
  // holds one, so the read presents the right X-Context-Grant-Id without a
@@ -65,21 +101,43 @@ export function buildReadPlan(inbox, grantsByScope) {
65
101
  // Reads — one call per place mail actually landed. The delivery names the
66
102
  // chat, agreement or task directly, so nothing has to be inferred from a scope.
67
103
  //
68
- // The task arm is not optional: a deliverable recorded against a task alone is
69
- // the shape the protocol asks for, and it lands in no chat and no agreement.
70
- // While this only looked at chatId/agreementId, such a delivery produced an
71
- // envelope whose plan was the ack and nothing else an agent following the
72
- // plan acked the deliverable without ever reading it.
104
+ // Total over BOTH axes, and that is the whole point of this shape:
105
+ //
106
+ // - over `kind`, so a delivery kind nobody plans a read for is a compile
107
+ // error here rather than a plan that quietly ends at the ack;
108
+ // - over the anchor, so is an artifact carrying none of chat/agreement/task
109
+ // — it is read as `artifact:<id>`, the entry ZIG-1037 added for that case.
110
+ //
111
+ // The first is history: while this looked at chatId/agreementId only, a
112
+ // task-bound deliverable produced an envelope whose plan was the ack and
113
+ // nothing else, and an agent following the plan acked work it never read. The
114
+ // second is that hole closed ahead of an emitter — nothing writes an
115
+ // anchor-less delivery today (ZIG-1100), and when something does it is planned.
73
116
  for (const d of deliveries) {
74
- if (d.kind === 'message' && d.chatId)
75
- read('messages', 'chat', d.chatId);
76
- else if (d.kind === 'artifact') {
77
- if (d.chatId)
78
- read('artifacts', 'chat', d.chatId);
79
- else if (d.agreementId)
80
- read('artifacts', 'agreement', d.agreementId);
81
- else if (d.taskId)
82
- read('artifacts', 'task', d.taskId);
117
+ switch (d.kind) {
118
+ case 'message':
119
+ // A message always lands in a chat; there is nowhere else to read it.
120
+ if (d.chatId)
121
+ read('messages', 'chat', d.chatId);
122
+ break;
123
+ case 'artifact':
124
+ read('artifacts', ...artifactEntry(d));
125
+ break;
126
+ case 'task-state':
127
+ case 'agreement':
128
+ // Deliberately no read call. These arrive as standing state elsewhere on
129
+ // the envelope — `tasksAwaitingMe` and `proposalsAwaitingMe` — and both
130
+ // already contribute their own plan entries above. A read here would be
131
+ // a duplicate of a call the agent has been handed.
132
+ break;
133
+ default:
134
+ // Compile-time exhaustiveness: a kind added to the vocabulary no longer
135
+ // satisfies `never`, so it cannot be introduced without a decision being
136
+ // made here. At runtime this skips rather than throws — this client is
137
+ // installed independently of the server it talks to, and one
138
+ // unrecognised delivery must not cost the caller the whole envelope.
139
+ d.kind;
140
+ break;
83
141
  }
84
142
  }
85
143
  // ZIG-660: reserve a slot for the ack before capping, so the pre-filled ack
@@ -180,16 +238,25 @@ export function indexReachByScope(reach) {
180
238
  * each read's covering grant (ZIG-635) so the agent can present
181
239
  * X-Context-Grant-Id without a separate discover round-trip.
182
240
  */
183
- export function formatInboxToolResult(inbox, ack, webOrigin, activeTasks, reach, activeTasksError) {
241
+ export function formatInboxToolResult(inbox, ack, webOrigin, activeTasks, reach, activeTasksError,
242
+ /**
243
+ * The caller's own ids (ZIG-1087). Optional here alone: this result carries
244
+ * the session-start COUNTS and a pointer to ziggs_pending_decisions, never
245
+ * the decision items themselves, and a count does not depend on which party
246
+ * may answer. Production callers pass it regardless — if this shape ever
247
+ * starts emitting `decisions`, they must already be marked correctly.
248
+ */
249
+ self = { agentId: '' }) {
184
250
  const byScope = reach?.length ? indexReachByScope(reach) : undefined;
185
- const { plan: readPlan, truncated: readPlanTruncated } = buildReadPlan(inbox, byScope);
251
+ const { plan: readPlan, truncated: readPlanTruncated } = buildReadPlan(inbox, byScope, self);
186
252
  const origin = resolveWebAppOrigin(webOrigin);
187
253
  // ZIG-659: the inbox reports session-start counts and points to
188
254
  // ziggs_pending_decisions for the sessionChatCard — it no longer re-emits the
189
255
  // cards, so a session start doesn't ship the same card ~6× across tools.
190
- const pending = formatPendingDecisionsPayload(inbox, origin, {
191
- activeTasks,
192
- activeTasksError,
256
+ // ZIG-1120: omit `activeTasks` so counts come from inbox.tasksAwaitingMe
257
+ // callers that still pass an array (or []) keep the listTasks-derived path.
258
+ const pending = formatPendingDecisionsPayload(inbox, origin, self, {
259
+ ...(activeTasks !== undefined ? { activeTasks, activeTasksError } : {}),
193
260
  withSessionCard: false,
194
261
  });
195
262
  const pendingTail = pending.hasActionable === true
@@ -1,5 +1,22 @@
1
- import type { InboxEnvelope, Task } from '@ziggs-ai/api-client';
1
+ import { type InboxEnvelope, type InboxTaskRef, type Task } from '@ziggs-ai/api-client';
2
+ /**
3
+ * The two ids this delegate answers for, in the order authority is checked:
4
+ * its own agent id (what the credential impersonates, and the only slot it can
5
+ * submit) then its principal's.
6
+ */
7
+ export interface DecisionSelfIds {
8
+ agentId: string;
9
+ ownerUserId?: string | null;
10
+ }
2
11
  export type PendingDecisionKind = 'proposal' | 'link_request';
12
+ /**
13
+ * Who is actually able to answer a decision (ZIG-1087).
14
+ *
15
+ * `agent` — the pending slot is this delegate's own; ziggs_agreement_respond works.
16
+ * `human` — the slot belongs to the principal. Consent is withheld from
17
+ * delegates on purpose, and no tool on this surface can give it.
18
+ */
19
+ export type DecisionResponder = 'agent' | 'human';
3
20
  export interface PendingDecisionItem {
4
21
  kind: PendingDecisionKind;
5
22
  /** Agreement id for proposals; requestId for link requests. */
@@ -9,12 +26,23 @@ export interface PendingDecisionItem {
9
26
  proposedAt: string | null;
10
27
  proposedAtLabel: string | null;
11
28
  appUrl: string;
12
- /** The MCP tool call the agent runs on approval (both kinds are in-chat). */
13
- respondApprove: string | null;
14
- respondReject: string | null;
15
- sayApprove: string | null;
16
- sayReject: string | null;
29
+ /**
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.
36
+ */
37
+ respondableBy: DecisionResponder;
17
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;
18
46
  export interface ActiveWorkItem {
19
47
  taskId: string;
20
48
  agreementId: string | null;
@@ -59,7 +87,7 @@ export declare function agreementsListAppUrl(origin: string): string;
59
87
  export declare function connectionsSettingsAppUrl(origin: string): string;
60
88
  /** Where the human decides paused transfers (ZIG-896). */
61
89
  export declare function walletAppUrl(origin: string): string;
62
- export declare function buildPendingDecisionItems(inbox: InboxEnvelope, webOrigin: string): PendingDecisionItem[];
90
+ export declare function buildPendingDecisionItems(inbox: InboxEnvelope, webOrigin: string, self: DecisionSelfIds): PendingDecisionItem[];
63
91
  /** ZIG-896 — shape pending payment approvals for the session payload/card. */
64
92
  export declare function buildPaymentApprovalItems(approvals: Array<Record<string, unknown>>, webOrigin: string): PaymentApprovalItem[];
65
93
  /**
@@ -77,6 +105,13 @@ export declare function buildPaymentApprovalItems(approvals: Array<Record<string
77
105
  */
78
106
  export declare function filterTasksForDelegate(tasks: Task[], selfIds: ReadonlySet<string>): Task[];
79
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[];
80
115
  /**
81
116
  * Combined card: approve/reject + active tasks (what humans actually need at
82
117
  * session start). ZIG-659: this is the ONE card shipped per session start —
@@ -90,7 +125,12 @@ export declare function buildSessionChatCard(decisions: PendingDecisionItem[], w
90
125
  paymentApprovals?: PaymentApprovalItem[];
91
126
  }): string;
92
127
  /** Structured session payload for MCP tools (ZIG-625 + active work). */
93
- export declare function formatPendingDecisionsPayload(inbox: InboxEnvelope, webOrigin: string, opts?: {
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
+ */
94
134
  activeTasks?: Task[];
95
135
  activeTasksError?: string;
96
136
  /**
@@ -1,3 +1,17 @@
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
+ }
1
15
  const TITLE_MAX = 72;
2
16
  const ACTIVE_TASK_LIMIT = 20;
3
17
  /**
@@ -57,61 +71,75 @@ function planProgress(plan) {
57
71
  const done = steps.filter((s) => s.status === 'completed' || s.status === 'skipped').length;
58
72
  return { done, total };
59
73
  }
60
- function proposalToItem(p, origin) {
61
- const id = p.agreementId;
62
- return {
63
- kind: 'proposal',
64
- agreementId: id,
65
- title: displayTitle(p.title?.trim() || '(untitled proposal)'),
66
- subtitle: null,
67
- proposedAt: p.proposedAt,
68
- proposedAtLabel: formatWhen(p.proposedAt),
69
- appUrl: agreementAppUrl(origin, id),
70
- respondApprove: `ziggs_agreement_respond agreementId=${id} action=approve`,
71
- respondReject: `ziggs_agreement_respond agreementId=${id} action=reject`,
72
- sayApprove: `approve ${id}`,
73
- sayReject: `reject ${id}`,
74
- };
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.
77
+ // Agent id first: when both owe a decision, ours is the one we can act on.
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';
75
83
  }
76
84
  function linkRequesterHeadline(c) {
77
- // ZIG-1039: consent cards lead with a readable name + org, id as fallback.
85
+ // ZIG-1039: consent cards lead with a readable name + org; ZIG-1137: ref is
86
+ // a non-addressable psn_* face, never an agent account id.
78
87
  const name = c.requesterDisplayName?.trim();
79
88
  const org = c.requesterOrgName?.trim();
80
89
  if (name && org)
81
90
  return `${name} (${org})`;
82
91
  if (name)
83
92
  return name;
84
- return c.requesterAgentId;
93
+ return c.requesterRef;
85
94
  }
86
- function linkToItem(c, origin) {
87
- const id = c.requestId;
88
- const note = c.message?.trim() || null;
89
- const headline = linkRequesterHeadline(c);
95
+ /** ZIG-1171 one builder for proposal / link_request decision items. */
96
+ function decisionToItem(kind, id, fields, origin, self) {
90
97
  return {
91
- kind: 'link_request',
98
+ kind,
92
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
+ }
119
+ function linkToItem(c, origin, self) {
120
+ const note = c.message?.trim() || null;
121
+ const headline = linkRequesterHeadline(c);
122
+ return decisionToItem('link_request', c.requestId, {
93
123
  title: truncateText(`Agent link · ${headline}`),
94
124
  subtitle: note
95
125
  ? truncateText(note, 96)
96
- : c.requesterAgentId && headline !== c.requesterAgentId
97
- ? truncateText(`id: ${c.requesterAgentId}`, 96)
126
+ : c.requesterRef && headline !== c.requesterRef
127
+ ? truncateText(`ref: ${c.requesterRef}`, 96)
98
128
  : null,
99
129
  proposedAt: c.requestedAt,
100
- proposedAtLabel: formatWhen(c.requestedAt),
101
- appUrl: agreementAppUrl(origin, id),
102
- respondApprove: `ziggs_agreement_respond agreementId=${id} action=approve`,
103
- respondReject: `ziggs_agreement_respond agreementId=${id} action=reject`,
104
- sayApprove: `approve link ${id}`,
105
- sayReject: `reject link ${id}`,
106
- };
130
+ facts: {
131
+ pendingApprovalPartyIds: c.pendingApprovalPartyIds,
132
+ proposedTo: c.proposedTo,
133
+ },
134
+ }, origin, self);
107
135
  }
108
- export function buildPendingDecisionItems(inbox, webOrigin) {
136
+ export function buildPendingDecisionItems(inbox, webOrigin, self) {
109
137
  const items = [];
110
138
  for (const p of inbox.proposalsAwaitingMe ?? []) {
111
- items.push(proposalToItem(p, webOrigin));
139
+ items.push(proposalToItem(p, webOrigin, self));
112
140
  }
113
141
  for (const c of inbox.connectionRequestsAwaitingMe ?? []) {
114
- items.push(linkToItem(c, webOrigin));
142
+ items.push(linkToItem(c, webOrigin, self));
115
143
  }
116
144
  return items;
117
145
  }
@@ -181,6 +209,30 @@ export function buildActiveWorkItems(tasks, webOrigin) {
181
209
  };
182
210
  });
183
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
+ }
184
236
  function kindLabel(kind) {
185
237
  if (kind === 'proposal')
186
238
  return 'Agreement proposal';
@@ -216,13 +268,19 @@ function buildDecisionSection(items, opts) {
216
268
  lines.push(`> ${item.subtitle}`);
217
269
  }
218
270
  lines.push('');
219
- if (item.sayApprove && item.respondApprove && item.sayReject && item.respondReject) {
220
- lines.push(`[Review in Ziggs →](${item.appUrl})`);
221
- lines.push('');
271
+ lines.push(`[Review in Ziggs →](${item.appUrl})`);
272
+ lines.push('');
273
+ const cues = decisionRespondCues(item);
274
+ if (cues) {
222
275
  lines.push('| You say in chat | What the agent runs |');
223
276
  lines.push('|:----------------|:--------------------|');
224
- lines.push(`| \`${item.sayApprove}\` | \`${item.respondApprove}\` |`);
225
- lines.push(`| \`${item.sayReject}\` | \`${item.respondReject}\` |`);
277
+ lines.push(`| \`${cues.sayApprove}\` | \`${cues.toolApprove}\` |`);
278
+ lines.push(`| \`${cues.sayReject}\` | \`${cues.toolReject}\` |`);
279
+ }
280
+ else {
281
+ // ZIG-1087 — this one is bound to the human's own party slot. Saying so
282
+ // beats offering a tool call that is refused every time.
283
+ lines.push('**Only you can answer this one** — it is waiting on your approval, not the agent\'s, so decide it on the link above. Nothing the agent runs can approve it for you.');
226
284
  }
227
285
  lines.push('');
228
286
  }
@@ -356,10 +414,12 @@ export function buildSessionChatCard(decisions, work, opts) {
356
414
  return lines.join('\n').trim();
357
415
  }
358
416
  /** Structured session payload for MCP tools (ZIG-625 + active work). */
359
- export function formatPendingDecisionsPayload(inbox, webOrigin, opts) {
417
+ export function formatPendingDecisionsPayload(inbox, webOrigin, self, opts) {
360
418
  const withSessionCard = opts?.withSessionCard !== false;
361
- const decisions = buildPendingDecisionItems(inbox, webOrigin);
362
- const activeWork = buildActiveWorkItems(opts?.activeTasks ?? [], webOrigin);
419
+ const decisions = buildPendingDecisionItems(inbox, webOrigin, self);
420
+ const activeWork = opts?.activeTasks !== undefined
421
+ ? buildActiveWorkItems(opts.activeTasks, webOrigin)
422
+ : buildActiveWorkItemsFromInbox(inbox.tasksAwaitingMe ?? [], webOrigin);
363
423
  const paymentApprovals = buildPaymentApprovalItems(opts?.paymentApprovals ?? [], webOrigin);
364
424
  const truncatedProposals = inbox.truncatedProposals ?? 0;
365
425
  const truncatedConnectionRequests = inbox.truncatedConnectionRequests ?? 0;
@@ -376,14 +436,25 @@ export function formatPendingDecisionsPayload(inbox, webOrigin, opts) {
376
436
  agreementsListAppUrl: listUrl,
377
437
  paymentApprovals,
378
438
  };
379
- const sessionChatCard = buildSessionChatCard(decisions, activeWork, cardOpts);
439
+ // Only the card owner ships it (ZIG-659). ziggs_inbox and ziggs_auth_status
440
+ // pass withSessionCard: false, so building it there assembled every decision,
441
+ // payment and up-to-20 work sections just to drop them for a one-line hint.
442
+ const sessionChatCard = withSessionCard
443
+ ? buildSessionChatCard(decisions, activeWork, cardOpts)
444
+ : null;
380
445
  const proposalCount = decisions.filter((d) => d.kind === 'proposal').length;
381
446
  const linkCount = decisions.filter((d) => d.kind === 'link_request').length;
447
+ // ZIG-1087 — decisions bound to the principal's own slot cannot be answered
448
+ // from this surface at all, so the instruction has to stop saying they can.
449
+ const humanOnly = decisions.filter((d) => d.respondableBy === 'human');
450
+ const humanOnlyNote = humanOnly.length
451
+ ? ` ${humanOnly.length} of them ${humanOnly.length === 1 ? 'is' : 'are'} the human's own to answer (respondableBy "human") — for those, hand over the link and do not call ziggs_agreement_respond; it is refused for a delegate.`
452
+ : '';
382
453
  const instruction = actionCount === 0
383
454
  ? 'No pending decisions or active tasks — continue with ziggs_inbox for scope news.'
384
455
  : withSessionCard
385
- ? 'Paste sessionChatCard at the top of your reply. Decisions: wait for explicit approve/reject before ziggs_agreement_respond. Tasks: when the human says work on <taskId>, read context and implement.'
386
- : `Counts only here — ${SESSION_CARD_POINTER} Decisions: wait for explicit approve/reject before ziggs_agreement_respond.`;
456
+ ? `Paste sessionChatCard at the top of your reply. Decisions: wait for explicit approve/reject before ziggs_agreement_respond.${humanOnlyNote} Tasks: when the human says work on <taskId>, read context and implement.`
457
+ : `Counts only here — ${SESSION_CARD_POINTER} Decisions: wait for explicit approve/reject before ziggs_agreement_respond.${humanOnlyNote}`;
387
458
  return {
388
459
  pendingCount,
389
460
  hasPending: pendingCount > 0,
@@ -444,11 +515,12 @@ export function buildPendingNextActions(decisions, work = []) {
444
515
  actions.push('Wait for the human to say approve/reject (e.g. `approve agr-…`) — do not auto-respond.');
445
516
  }
446
517
  for (const d of decisions.slice(0, 4)) {
447
- if (d.sayApprove && d.sayReject) {
448
- 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}\``);
449
521
  }
450
522
  else {
451
- actions.push(`${kindLabel(d.kind)} ${d.agreementId}: connect or reject in the browser ${d.appUrl}`);
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.`);
452
524
  }
453
525
  }
454
526
  for (const w of work.slice(0, 4)) {
@@ -14,9 +14,15 @@ export interface ToolErrorShape {
14
14
  hint?: string;
15
15
  }
16
16
  /** Classify a raw client error message into a stable shape. */
17
- export declare function classifyToolError(rawMessage: string): ToolErrorShape;
18
- /** MCP tool error result: machine-readable code + cleaned message (+ hint). */
19
- export declare function toolError(message: string): {
17
+ export declare function classifyToolError(rawMessage: string, knownStatus?: number | null): ToolErrorShape;
18
+ /**
19
+ * MCP tool error result: machine-readable code + cleaned message (+ hint).
20
+ *
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.
24
+ */
25
+ export declare function toolError(input: unknown): {
20
26
  content: {
21
27
  type: "text";
22
28
  text: string;
package/dist/toolError.js CHANGED
@@ -14,6 +14,14 @@ const SCOPE_DENIED_HINT = 'You are not authorized for this scope. To get access:
14
14
  'to issue you a context grant (they run ziggs_context_issue_grant), or propose a ' +
15
15
  'bilateral link first (ziggs_agreement_propose with engagementKind "link"). Check what you can already ' +
16
16
  'reach with ziggs_grant_list / ziggs_context_snapshot.';
17
+ // ZIG-1088 — a human-authority denial is not "try again with more scope": no
18
+ // retry by this caller can ever pass it, because the guard refuses on being an
19
+ // agent at all, before it looks at what was asked. Without a next action the
20
+ // only strategy left is repetition, which is what agents did.
21
+ const HUMAN_AUTHORITY_HINT = 'This action is the account owner\'s to take, not yours — retrying will not ' +
22
+ 'change that. Your paths: ziggs_context_delegate to pass on a narrower slice ' +
23
+ 'of a grant you ALREADY hold, ziggs_artifact_share for an artifact you ' +
24
+ 'authored, or tell your human to do it in the Ziggs app.';
17
25
  function codeForStatus(status) {
18
26
  if (status === 401)
19
27
  return 'NOT_AUTHENTICATED';
@@ -45,11 +53,27 @@ function extractBodyReason(raw) {
45
53
  return null;
46
54
  }
47
55
  }
56
+ /**
57
+ * An `ApiError` from api-client, recognised by shape rather than `instanceof`:
58
+ * a bundled or duplicated copy of the class would fail an identity check while
59
+ * carrying exactly the fields we need.
60
+ */
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;
66
+ }
48
67
  /** Classify a raw client error message into a stable shape. */
49
- export function classifyToolError(rawMessage) {
68
+ export function classifyToolError(rawMessage, knownStatus) {
50
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.
51
75
  const statusMatch = HTTP_STATUS.exec(cleaned);
52
- const status = statusMatch ? Number(statusMatch[1]) : null;
76
+ const status = knownStatus ?? (statusMatch ? Number(statusMatch[1]) : null);
53
77
  const bodyReason = extractBodyReason(cleaned);
54
78
  if (status === null) {
55
79
  return { code: 'TOOL_ERROR', message: cleaned || rawMessage };
@@ -57,6 +81,16 @@ export function classifyToolError(rawMessage) {
57
81
  const code = codeForStatus(status);
58
82
  // Prefer the backend's own reason over the transport framing.
59
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.
87
+ if (code === 'NOT_AUTHORIZED' && /impersonated agent cannot perform/i.test(message)) {
88
+ return {
89
+ code: 'AGENT_LACKS_HUMAN_AUTHORITY',
90
+ message,
91
+ hint: HUMAN_AUTHORITY_HINT,
92
+ };
93
+ }
60
94
  // Context-scope denials (the backend says "…for this scope") get the scope
61
95
  // code and a recovery path. Other 403s (connection grants, party checks)
62
96
  // keep the generic code — their fixes live in other domains.
@@ -69,9 +103,18 @@ export function classifyToolError(rawMessage) {
69
103
  }
70
104
  return { code, message };
71
105
  }
72
- /** MCP tool error result: machine-readable code + cleaned message (+ hint). */
73
- export function toolError(message) {
74
- const shape = classifyToolError(message);
106
+ /**
107
+ * MCP tool error result: machine-readable code + cleaned message (+ hint).
108
+ *
109
+ * 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.
112
+ */
113
+ 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));
75
118
  return {
76
119
  content: [
77
120
  { type: 'text', text: JSON.stringify({ error: shape }, null, 2) },
package/dist/tools.js CHANGED
@@ -1,11 +1,11 @@
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';
7
7
  import { formatInboxToolResult, buildReadContextReadPlan, } from './inboxToolResult.js';
8
- import { filterTasksForDelegate, formatPendingDecisionsPayload, resolveWebAppOrigin, buildPendingNextActions, } from './pendingDecisions.js';
8
+ import { agreementAppUrl, filterTasksForDelegate, formatPendingDecisionsPayload, resolveWebAppOrigin, buildPendingNextActions, } from './pendingDecisions.js';
9
9
  import { PROTOCOL } from './protocol/delegateProtocol.js';
10
10
  const RELAY_COORDINATOR_AGENT_ID = 'relay-coordinator';
11
11
  function buildRelayCoordinatorTaskBody(opts) {
@@ -58,6 +58,12 @@ const ZIGGS_RECORD_ARTIFACT_DESCRIPTION = 'Write an artifact. Scope is optional
58
58
  // grouped connection lister (ConnectionsClient.listForHolder), the org lookups
59
59
  // (fetchMyOrgs / fetchDelegateAccess), and the whole SDK-twin tool definitions
60
60
  // all live in @ziggs-ai/api-client now (shared with the agent SDK).
61
+ /** The human this delegate acts for: operator-key ownerId, else the config id. */
62
+ function ownerPrincipalId(creds, cfg) {
63
+ return (decodeOperatorKeyClaims(creds.operatorKey)?.ownerId ??
64
+ cfg.ZIGGS_OWNER_USER_ID ??
65
+ null);
66
+ }
61
67
  /**
62
68
  * Ids this delegate answers for: its own agent id plus its principal's user
63
69
  * id (operator-key ownerId / ZIGGS_OWNER_USER_ID). Used to decide which
@@ -75,29 +81,42 @@ function delegateSelfIds(creds, cfg) {
75
81
  async function loadSessionActionsPayload(creds, cfg, opts) {
76
82
  const webOrigin = resolveWebAppOrigin(cfg.ZIGGS_WEB_URL);
77
83
  const client = new InboxClient(creds.operatorKey, creds.agentId);
78
- 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;
79
96
  let activeTasks = [];
80
97
  let activeTasksError;
81
- try {
82
- const listed = await listTasks({ state: 'active', limit: 20 }, creds);
83
- activeTasks = filterTasksForDelegate(listed.tasks ?? [], delegateSelfIds(creds, cfg));
98
+ if (tasksSettled.status === 'fulfilled') {
99
+ activeTasks = filterTasksForDelegate(tasksSettled.value.tasks ?? [], delegateSelfIds(creds, cfg));
84
100
  }
85
- catch (e) {
101
+ else {
86
102
  // ZIG-700 — inbox is still useful when task listing fails, but surface the
87
103
  // failure so hasActiveWork:false is not mistaken for "no tasks".
88
- activeTasksError = e.message;
104
+ activeTasksError = tasksSettled.reason.message;
89
105
  }
90
106
  // ZIG-896 — paused transfers awaiting the wallet owner. Same failure rule as
91
107
  // tasks: a failed fetch is surfaced, not silently rendered as "none pending".
92
108
  let paymentApprovals = [];
93
109
  let paymentApprovalsError;
94
- try {
95
- paymentApprovals = (await new PaymentsClient(creds.operatorKey, creds.agentId).approvals({ status: 'pending' }));
110
+ if (paymentsSettled.status === 'fulfilled') {
111
+ paymentApprovals = paymentsSettled.value;
96
112
  }
97
- catch (e) {
98
- paymentApprovalsError = e.message;
113
+ else {
114
+ paymentApprovalsError = paymentsSettled.reason.message;
99
115
  }
100
116
  return formatPendingDecisionsPayload(inbox, webOrigin, {
117
+ agentId: creds.agentId,
118
+ ownerUserId: ownerPrincipalId(creds, cfg),
119
+ }, {
101
120
  activeTasks,
102
121
  activeTasksError,
103
122
  paymentApprovals,
@@ -163,7 +182,7 @@ function registerMarketplaceTools(server, creds) {
163
182
  });
164
183
  }
165
184
  catch (e) {
166
- return toolError(e.message);
185
+ return toolError(e);
167
186
  }
168
187
  });
169
188
  }
@@ -177,7 +196,7 @@ function registerConnectionTools(server, creds) {
177
196
  return textResult({ connections });
178
197
  }
179
198
  catch (e) {
180
- return toolError(e.message);
199
+ return toolError(e);
181
200
  }
182
201
  });
183
202
  registerCapability(server, requestConnectionCapability, creds);
@@ -186,13 +205,20 @@ export function registerZiggsTools(server, creds, cfg) {
186
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 () => {
187
206
  const claims = decodeOperatorKeyClaims(creds.operatorKey);
188
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
+ ]);
189
215
  let actingOrgId = null;
190
216
  let actingOrgName = null;
191
217
  let actingOrgKind = null;
192
218
  let connected = false;
193
219
  let switchOrgHint = null;
194
- try {
195
- const access = await fetchDelegateAccess(creds);
220
+ if (accessSettled.status === 'fulfilled') {
221
+ const access = accessSettled.value;
196
222
  actingOrgId = typeof access.orgId === 'string' ? access.orgId : null;
197
223
  actingOrgName = typeof access.orgName === 'string' ? access.orgName : null;
198
224
  actingOrgKind = typeof access.orgKind === 'string' ? access.orgKind : null;
@@ -200,8 +226,8 @@ export function registerZiggsTools(server, creds, cfg) {
200
226
  switchOrgHint =
201
227
  typeof access.switchOrgHint === 'string' ? access.switchOrgHint : null;
202
228
  }
203
- catch (e) {
204
- switchOrgHint = `Could not load runtime org: ${e.message}`;
229
+ else {
230
+ switchOrgHint = `Could not load runtime org: ${accessSettled.reason.message}`;
205
231
  }
206
232
  let pendingDecisions = {
207
233
  pendingCount: 0,
@@ -211,14 +237,10 @@ export function registerZiggsTools(server, creds, cfg) {
211
237
  actionCount: 0,
212
238
  hasActionable: false,
213
239
  };
214
- try {
215
- // ZIG-659: counts + a pointer only — the full sessionChatCard is owned
216
- // by ziggs_pending_decisions, not duplicated here.
217
- pendingDecisions = await loadSessionActionsPayload(creds, cfg, {
218
- withSessionCard: false,
219
- });
240
+ if (pendingSettled.status === 'fulfilled') {
241
+ pendingDecisions = pendingSettled.value;
220
242
  }
221
- catch {
243
+ else {
222
244
  pendingDecisions = {
223
245
  ...pendingDecisions,
224
246
  fetchError: 'Could not load inbox/tasks — call ziggs_pending_decisions.',
@@ -260,7 +282,7 @@ export function registerZiggsTools(server, creds, cfg) {
260
282
  return textResult({ count: orgs.length, orgs });
261
283
  }
262
284
  catch (e) {
263
- return toolError(e.message);
285
+ return toolError(e);
264
286
  }
265
287
  });
266
288
  server.tool('ziggs_pending_decisions', ZIGGS_PENDING_DECISIONS_DESCRIPTION, {}, READ_ONLY, async () => {
@@ -277,7 +299,7 @@ export function registerZiggsTools(server, creds, cfg) {
277
299
  return textResult(payload);
278
300
  }
279
301
  catch (e) {
280
- return toolError(e.message);
302
+ return toolError(e);
281
303
  }
282
304
  });
283
305
  if (cfg.debugTools) {
@@ -299,7 +321,7 @@ export function registerZiggsTools(server, creds, cfg) {
299
321
  });
300
322
  }
301
323
  catch (e) {
302
- return toolError(e.message);
324
+ return toolError(e);
303
325
  }
304
326
  });
305
327
  }
@@ -320,7 +342,7 @@ export function registerZiggsTools(server, creds, cfg) {
320
342
  return textResult(result);
321
343
  }
322
344
  catch (e) {
323
- return toolError(e.message);
345
+ return toolError(e);
324
346
  }
325
347
  });
326
348
  server.tool('ziggs_agreement_list', 'List agreements you are a party to — your hires, proposals, and work (default scope "mine"). Pass scope "reachable" to list every agreement your grant can read in the org, including ones you are not a party to; the isYou flags on each row mark which party (if any) is you.', {
@@ -341,7 +363,7 @@ export function registerZiggsTools(server, creds, cfg) {
341
363
  return textResult({ count: agreements.length, scope: scope ?? 'mine', agreements });
342
364
  }
343
365
  catch (e) {
344
- return toolError(e.message);
366
+ return toolError(e);
345
367
  }
346
368
  });
347
369
  server.tool('ziggs_agreement_get', 'Fetch a single agreement by id.', { agreementId: z.string() }, READ_ONLY, async ({ agreementId }) => {
@@ -352,7 +374,7 @@ export function registerZiggsTools(server, creds, cfg) {
352
374
  return textResult({ agreement });
353
375
  }
354
376
  catch (e) {
355
- return toolError(e.message);
377
+ return toolError(e);
356
378
  }
357
379
  });
358
380
  server.tool('ziggs_chat_list', 'List chats the delegate agent is a member of (GET /chats/mine).', {}, READ_ONLY, async () => {
@@ -361,7 +383,7 @@ export function registerZiggsTools(server, creds, cfg) {
361
383
  return textResult({ count: chats.length, chats });
362
384
  }
363
385
  catch (e) {
364
- return toolError(e.message);
386
+ return toolError(e);
365
387
  }
366
388
  });
367
389
  registerCapability(server, openConversationCapability, creds);
@@ -370,7 +392,7 @@ export function registerZiggsTools(server, creds, cfg) {
370
392
  receiverId: z
371
393
  .string()
372
394
  .optional()
373
- .describe("User or agent id receiving the message. Optional: with exactly one other member the recipient is inferred server-side; in a room with several members an omitted receiver becomes a broadcast to the room's HUMAN members (agents are not woken by it). Pass 'human' to broadcast explicitly, or a specific agent id to address (and wake) that agent."),
395
+ .describe("Receiver id for the message. Prefer a real user/agent id when you have one. Cross-org masked counterparties arrive as opaque `rpb_*` refs — echo that same ref back here; the backend resolves it in-room (do not look it up, wake, or pay against it). Optional: with exactly one other member the recipient is inferred server-side; in a room with several members an omitted receiver becomes a broadcast to the room's HUMAN members (agents are not woken by it). Pass 'human' to broadcast explicitly."),
374
396
  text: z.string(),
375
397
  entryType: z
376
398
  .string()
@@ -398,7 +420,7 @@ export function registerZiggsTools(server, creds, cfg) {
398
420
  return textResult(result);
399
421
  }
400
422
  catch (e) {
401
- return toolError(e.message);
423
+ return toolError(e);
402
424
  }
403
425
  });
404
426
  server.tool('ziggs_agreement_propose', 'Propose an agreement — direct, broadcast, hand-off, or link; there are no separate publish tools. DIRECT: proposedTo = one counterparty id, chatId required. Omit providerId (or set it to proposedTo) to commission the recipient (they work, your side pays); set providerId to your own agent id to offer (you work, proposedTo pays); a third-party providerId brokers (they work, proposedTo pays) and requires that provider to have a matching active offer. BROADCAST: proposedTo "everyone" (fully public) or "org" (your active org only), chatId optional — with no providerId this publishes a QUEST (whoever claims does the work, your side pays); with providerId = your own id it publishes a STANDING OFFER (you work, the claimer pays). HAND-OFF (share an agent you hired): set parentAgreementId = that ACTIVE hire and providerId = its provider; proposedTo may be "everyone"/"org" (claimable) or a specific beneficiary id (they approve directly). The provider stays pinned — whoever claims/approves is the CUSTOMER the work is done for, never the worker, and on a priced hand-off they are also the payer (price omitted/0 = free: nobody is billed for their tasks). Handing off someone else\'s agent leaves that provider\'s approval pending — it must accept once before the hand-off can be claimed. Claiming is ziggs_agreement_claim; browsing is ziggs_marketplace_view. LINK: engagementKind "link" with proposedTo = an agent id proposes bilateral trust (no chat, no money). The server routes parties.proposedTo to that agent\'s owner human (a person decides who their delegate trusts) — the id you pass may differ from parties.proposedTo in the response; when it does, `note` explains the rewrite. Approve via ziggs_agreement_respond. ROLES: proposedTo is the CUSTOMER — the party the work is done for; the payer is only who pays, always derived server-side as the non-providing side — there is no payer input. engagementKind "service" (default) = one deliverable; "hire" = ongoing engagement. Agreements are STANDING by default (lifecycle "open": no expiry, unlimited tasks) — hire once, then keep spawning tasks under the same agreement; set expiresAt (time-bound) or maxExecutions (count-bound) only when the engagement should end on its own. price is recorded on the agreement but does not itself trigger a transfer.', {
@@ -417,8 +439,10 @@ export function registerZiggsTools(server, creds, cfg) {
417
439
  price: z
418
440
  .number()
419
441
  .optional()
420
- .describe('Amount in CENTS — 500 means $5.00. Optional; does not trigger a transfer by itself. ' +
421
- 'When you quote it to a human, convert: saying "$500" for 500 is off by 100x.'),
442
+ .describe('Amount in CENTS — 500 means $5.00, and ϟ5.00 in the UI. Optional; does not trigger a ' +
443
+ 'transfer by itself. Convert BOTH ways or you are off by 100x: a human who says ' +
444
+ '"ϟ2" or "$2 per task" means price 200, not 2; quoting 500 back as "$500" or "ϟ500" ' +
445
+ 'is the same mistake inverted. ϟ is the currency symbol the UI shows — it is not cents.'),
422
446
  engagementKind: z
423
447
  .enum(['hire', 'service', 'link'])
424
448
  .optional()
@@ -455,8 +479,6 @@ export function registerZiggsTools(server, creds, cfg) {
455
479
  lifecycle,
456
480
  billing,
457
481
  }, creds);
458
- // ZIG-957: link mutations return linkSummary, not the raw Mongo doc.
459
- const agreementOut = shape === 'link' && agreement ? linkSummary(agreement) : agreement;
460
482
  // ZIG-1039: surface the owner-routing rewrite so agents do not think
461
483
  // the id they passed was ignored silently.
462
484
  const routedProposedTo = shape === 'link' &&
@@ -467,7 +489,7 @@ export function registerZiggsTools(server, creds, cfg) {
467
489
  : undefined;
468
490
  return textResult({
469
491
  shape,
470
- agreement: agreementOut,
492
+ agreement,
471
493
  ...(routedProposedTo ? { note: routedProposedTo } : {}),
472
494
  ...(shape === 'quest' || shape === 'offer'
473
495
  ? {
@@ -477,7 +499,7 @@ export function registerZiggsTools(server, creds, cfg) {
477
499
  });
478
500
  }
479
501
  catch (e) {
480
- return toolError(e.message);
502
+ return toolError(e);
481
503
  }
482
504
  });
483
505
  registerCapability(server, agreementClaimCapability, creds);
@@ -486,7 +508,6 @@ export function registerZiggsTools(server, creds, cfg) {
486
508
  executorId: z.string().describe('Agent doing the delegated work'),
487
509
  chatId: z.string().describe('Chat the delegation is coordinated in'),
488
510
  description: z.string().describe('What the sub-agreement covers'),
489
- parentTaskId: z.string().optional(),
490
511
  price: z
491
512
  .number()
492
513
  .optional()
@@ -498,14 +519,13 @@ export function registerZiggsTools(server, creds, cfg) {
498
519
  .optional()
499
520
  .describe("Usually inferred: expiresAt → 'time-bound', maxExecutions → 'count-bound', neither → 'open' (standing)."),
500
521
  agreementDescription: z.string().optional(),
501
- }, WRITE, async ({ parentAgreementId, executorId, chatId, description, parentTaskId, price, expiresAt, maxExecutions, lifecycle, agreementDescription, }) => {
522
+ }, WRITE, async ({ parentAgreementId, executorId, chatId, description, price, expiresAt, maxExecutions, lifecycle, agreementDescription, }) => {
502
523
  try {
503
524
  const agreement = await delegateAgreement({
504
525
  parentAgreementId,
505
526
  executorId,
506
527
  chatId,
507
528
  description,
508
- parentTaskId,
509
529
  price,
510
530
  expiresAt,
511
531
  maxExecutions,
@@ -515,13 +535,13 @@ export function registerZiggsTools(server, creds, cfg) {
515
535
  return textResult({ agreement });
516
536
  }
517
537
  catch (e) {
518
- return toolError(e.message);
538
+ return toolError(e);
519
539
  }
520
540
  });
521
541
  if (!cfg.coreOnly) {
522
542
  registerMarketplaceTools(server, creds);
523
543
  }
524
- server.tool('ziggs_agreement_respond', 'Approve or reject a pending DIRECT agreement proposal addressed to you (PUT /approvals/:partyId). Open broadcasts (quests, standing offers, link invites) have no personal approval slot — claim those with ziggs_agreement_claim instead, or ignore them to pass. ONE exception: a hand-off that pins YOU (or your agent) as provider carries your pending approval even as an open broadcast — approving it consents to serving whoever claims it (the row stays open for claims); rejecting cancels the hand-off.', {
544
+ server.tool('ziggs_agreement_respond', 'Approve or reject a pending DIRECT agreement proposal addressed to YOU — your own party slot (PUT /approvals/:partyId, which takes a decision only from the party itself). A proposal bound to your PRINCIPAL instead is not yours to answer and this tool refuses it: consent is deliberately withheld from delegates, so no retry and no other tool changes it — the human decides it in the Ziggs app, under Agreements. ziggs_pending_decisions marks which is which with respondableBy (agent | human), so check there before calling. Open broadcasts (quests, standing offers, link invites) have no personal approval slot — claim those with ziggs_agreement_claim instead, or ignore them to pass. ONE exception: a hand-off that pins YOU (or your agent) as provider carries your pending approval even as an open broadcast — approving it consents to serving whoever claims it (the row stays open for claims); rejecting cancels the hand-off.', {
525
545
  agreementId: z.string(),
526
546
  action: z.enum(['approve', 'reject']),
527
547
  }, WRITE, async ({ agreementId, action }) => {
@@ -530,13 +550,13 @@ export function registerZiggsTools(server, creds, cfg) {
530
550
  const ownerId = claims?.ownerId ?? cfg.ZIGGS_OWNER_USER_ID;
531
551
  const updated = await respondToAgreement(agreementId, action, creds, {
532
552
  ownerUserId: ownerId,
553
+ // So a refusal can name where the human goes, not just why.
554
+ appUrl: agreementAppUrl(resolveWebAppOrigin(cfg.ZIGGS_WEB_URL), agreementId),
533
555
  });
534
- // ZIG-957: link approvals/rejects share the same summary shape as claim/revoke.
535
- const agreement = updated?.engagementKind === 'link' ? linkSummary(updated) : updated;
536
- return textResult({ agreement });
556
+ return textResult({ agreement: updated });
537
557
  }
538
558
  catch (e) {
539
- return toolError(e.message);
559
+ return toolError(e);
540
560
  }
541
561
  });
542
562
  server.tool('ziggs_agreement_revoke', 'Revoke any agreement you are a party to — hire, service, quest, standing offer, or link (DELETE /agreements/:id). Either party may revoke; this ends the engagement immediately. Revoking a link ends cross-org reach to that peer; revoking an open broadcast takes it off the marketplace.', {
@@ -545,26 +565,25 @@ export function registerZiggsTools(server, creds, cfg) {
545
565
  try {
546
566
  const result = await revokeAgreement(agreementId, creds);
547
567
  const isLink = result.agreement?.engagementKind === 'link';
548
- // ZIG-957: same summary shape as create/list/claim link tools (not the raw Mongo doc).
549
- const agreement = result.agreement
550
- ? (isLink ? linkSummary(result.agreement) : result.agreement)
551
- : undefined;
552
568
  return textResult({
553
569
  status: 'revoked',
554
570
  agreementId,
555
571
  ...(isLink
556
572
  ? { note: 'Link revoked — unpublished cross-org reach to this peer is blocked again.' }
557
573
  : {}),
558
- agreement,
574
+ agreement: result.agreement,
559
575
  });
560
576
  }
561
577
  catch (e) {
562
- return toolError(e.message);
578
+ return toolError(e);
563
579
  }
564
580
  });
565
- server.tool('ziggs_agreement_counter', 'Counter a pending proposal with revised terms instead of approving or rejecting (POST /agreements/:id/counter). Provide only the terms you want to change — price, description, expiry, lifecycle, or plan; omitted fields keep the original proposal\'s value. The counter goes back to the counterparty as a fresh pending proposal for them to approve/reject/counter. Read the current terms first with ziggs_agreement_get.', {
581
+ server.tool('ziggs_agreement_counter', 'Counter a pending proposal with revised terms instead of approving or rejecting (POST /agreements/:id/counter). Provide only the terms you want to change — price, description, expiry, or lifecycle; omitted fields keep the original proposal\'s value. The counter goes back to the counterparty as a fresh pending proposal for them to approve/reject/counter. Read the current terms first with ziggs_agreement_get.', {
566
582
  agreementId: z.string().describe('The pending agreement to counter'),
567
- price: z.number().optional().describe('Revised price'),
583
+ price: z
584
+ .number()
585
+ .optional()
586
+ .describe('Revised price, in CENTS — 500 means $5.00 / ϟ5.00 (see ziggs_agreement_propose).'),
568
587
  agreementDescription: z
569
588
  .string()
570
589
  .optional()
@@ -579,22 +598,13 @@ export function registerZiggsTools(server, creds, cfg) {
579
598
  .string()
580
599
  .optional()
581
600
  .describe('Revised task description for the spawned work'),
582
- plan: z
583
- .record(z.unknown())
584
- .optional()
585
- .describe('Override plan { steps: [...] }; omit to keep the original'),
586
- planReviewTiming: z
587
- .enum(['with_proposal', 'before_execution'])
588
- .optional()
589
- .describe('When the buyer reviews the plan: with_proposal | before_execution'),
590
- requireMidWorkPlanAck: z.boolean().optional(),
591
601
  }, WRITE, async ({ agreementId, ...counter }) => {
592
602
  try {
593
603
  const agreement = await counterAgreement(agreementId, counter, creds);
594
604
  return textResult({ status: 'countered', agreementId, agreement });
595
605
  }
596
606
  catch (e) {
597
- return toolError(e.message);
607
+ return toolError(e);
598
608
  }
599
609
  });
600
610
  server.tool('ziggs_agreement_fulfill', 'END an agreement you PROVIDE — permanently (POST /agreements/:id/fulfill). Fulfilling terminates the whole relationship, not one deliverable: every grant the agreement conferred (context, connection, payment) is revoked, its shared space is torn down, and it cannot be reopened — the counterparty would have to re-hire you from scratch. Finished WORK is reported with ziggs_task_set_result, which closes the task and leaves the agreement standing for the next one. Only fulfill a count/time-bound engagement whose full scope is delivered and where nothing more is expected — never a standing hire that just finished a task. Party-gated server-side: only the providing side can fulfill.', {
@@ -605,7 +615,7 @@ export function registerZiggsTools(server, creds, cfg) {
605
615
  return textResult({ status: 'fulfilled', agreementId, agreement: result.agreement });
606
616
  }
607
617
  catch (e) {
608
- return toolError(e.message);
618
+ return toolError(e);
609
619
  }
610
620
  });
611
621
  server.tool('ziggs_inbox', ZIGGS_INBOX_DESCRIPTION, {
@@ -620,38 +630,36 @@ export function registerZiggsTools(server, creds, cfg) {
620
630
  }, READ_ONLY, async ({ ack, waitSeconds }) => {
621
631
  try {
622
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.
623
635
  const acked = ack ? await client.ack(ack) : null;
624
636
  const inbox = await client.getInbox(waitSeconds != null ? { waitSeconds } : {});
625
- let activeTasks = [];
626
- let activeTasksError;
627
- try {
628
- const listed = await listTasks({ state: 'active', limit: 20 }, creds);
629
- activeTasks = filterTasksForDelegate(listed.tasks ?? [], delegateSelfIds(creds, cfg));
630
- }
631
- catch (e) {
632
- // ZIG-700 — surface the failure instead of silently omitting the work card.
633
- activeTasksError = e.message;
634
- }
635
- // ZIG-635: tag each scope with the covering grant from the caller's own
636
- // held context grants. Best-effort — the inbox still works untagged.
637
- // All pages of live grants only (GET /grants also returns expired/revoked
638
- // 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).
639
641
  let reach = [];
640
- try {
641
- reach = await new GrantsClient(creds.operatorKey, creds.agentId).listAllGrants({
642
- // Every context scope kind a literal subset here is how the CLI
643
- // silently dropped a whole kind when `artifact` was added.
644
- scopeKind: [...CONTEXT_GRANT_SCOPE_KINDS],
645
- health: 'active',
646
- });
647
- }
648
- catch {
649
- // 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
+ }
650
658
  }
651
- return textResult(formatInboxToolResult(inbox, acked, cfg.ZIGGS_WEB_URL, activeTasks, reach, activeTasksError));
659
+ return textResult(formatInboxToolResult(inbox, acked, cfg.ZIGGS_WEB_URL, undefined, reach, undefined, { agentId: creds.agentId, ownerUserId: ownerPrincipalId(creds, cfg) }));
652
660
  }
653
661
  catch (e) {
654
- return toolError(e.message);
662
+ return toolError(e);
655
663
  }
656
664
  });
657
665
  registerCapabilities(server, GRANTS_CAPABILITIES, creds);
@@ -681,7 +689,7 @@ export function registerZiggsTools(server, creds, cfg) {
681
689
  // ---------------------------------------------------------------------------
682
690
  // Task mutation tools (ZIG-555)
683
691
  // ---------------------------------------------------------------------------
684
- server.tool('ziggs_task_create', 'Create a task under an agreement. Every task belongs to exactly one agreement (agreementId required).', {
692
+ server.tool('ziggs_task_create', 'Create a task under an agreement. Every task belongs to exactly one agreement (agreementId required). Pass plan to give the task its checklist in the same call — every step needs a non-blank description, since that is the label whoever is watching reads before anything closes. Leave plan off to start without one and post it later with ziggs_task_replace_plan.', {
685
693
  agreementId: z.string().describe('Agreement this task belongs to'),
686
694
  description: z.string().describe('What the task entails'),
687
695
  parentTaskId: z.string().optional().describe('Parent task id for sub-tasks'),
@@ -693,13 +701,47 @@ export function registerZiggsTools(server, creds, cfg) {
693
701
  .array(z.string())
694
702
  .optional()
695
703
  .describe('Artifact ids this task consumes as structured inputs — pass prior-step output handles without embedding them in description'),
696
- }, WRITE, async ({ agreementId, description, parentTaskId, assigneeId, inputArtifactIds }) => {
704
+ // ZIG-1096: POST /tasks and the SDK's task_create have always taken
705
+ // these three; only this surface hid them, so an agent on MCP had to
706
+ // create-then-replace_plan even when it already knew the steps.
707
+ plan: z
708
+ .object({
709
+ steps: z
710
+ .array(z.object({
711
+ stepId: z.string().describe('Stable id for this step (e.g. uuid)'),
712
+ description: z
713
+ .string()
714
+ .describe('What this step does, in one line — required and non-blank'),
715
+ order: z.number().int(),
716
+ }))
717
+ .describe('Ordered steps the task starts with'),
718
+ })
719
+ .optional()
720
+ .describe('The plan the task is born with. Omit to start without one.'),
721
+ planReviewTiming: z
722
+ .enum(['with_proposal', 'before_execution'])
723
+ .optional()
724
+ .describe('When the buyer reviews the plan. with_proposal (default) — approving the agreement covers it. before_execution — the task cannot start until the plan is acknowledged.'),
725
+ requireMidWorkPlanAck: z
726
+ .boolean()
727
+ .optional()
728
+ .describe('When true, restructuring the plan mid-task parks it for a fresh acknowledgement instead of applying silently.'),
729
+ }, WRITE, async ({ agreementId, description, parentTaskId, assigneeId, inputArtifactIds, plan, planReviewTiming, requireMidWorkPlanAck, }) => {
697
730
  try {
698
- const task = await createTask({ agreementId, description, parentTaskId, assigneeId, inputArtifactIds }, creds);
731
+ const task = await createTask({
732
+ agreementId,
733
+ description,
734
+ parentTaskId,
735
+ assigneeId,
736
+ inputArtifactIds,
737
+ plan,
738
+ planReviewTiming,
739
+ requireMidWorkPlanAck,
740
+ }, creds);
699
741
  return textResult({ ok: true, task });
700
742
  }
701
743
  catch (e) {
702
- return toolError(e.message);
744
+ return toolError(e);
703
745
  }
704
746
  });
705
747
  server.tool('ziggs_task_set_result', 'Transition a task to a terminal state (completed / failed / cancelled) and record the result. Enforces the state machine — only active tasks can be transitioned.', {
@@ -725,7 +767,7 @@ export function registerZiggsTools(server, creds, cfg) {
725
767
  return textResult({ ok: true, task });
726
768
  }
727
769
  catch (e) {
728
- return toolError(e.message);
770
+ return toolError(e);
729
771
  }
730
772
  });
731
773
  server.tool('ziggs_task_replace_plan', 'Replace the plan for a task with the full ordered step list you provide — existing steps are replaced wholesale, not appended to. Use this to post progress: resend the whole plan with completed steps marked in their descriptions.', {
@@ -743,7 +785,7 @@ export function registerZiggsTools(server, creds, cfg) {
743
785
  return textResult({ ok: true, task });
744
786
  }
745
787
  catch (e) {
746
- return toolError(e.message);
788
+ return toolError(e);
747
789
  }
748
790
  });
749
791
  server.tool('ziggs_task_list', 'List tasks reachable by this delegate agent (GET /tasks). Scope is determined by the operator key — same reach as chats and agreements. Supports optional state filter, cursor pagination, and assignee filtering.', {
@@ -768,7 +810,7 @@ export function registerZiggsTools(server, creds, cfg) {
768
810
  return textResult(result);
769
811
  }
770
812
  catch (e) {
771
- return toolError(e.message);
813
+ return toolError(e);
772
814
  }
773
815
  });
774
816
  server.tool('ziggs_task_get', 'Fetch a single task by id (GET /tasks/:id). Use this when a human hands you a taskId directly (e.g. "work on task_…") so you can read the work-order — its description, plan, assignee, state, and result — before acting. Same operator-key scope as ziggs_task_list; pairs with ziggs_task_set_result to close the task.', { taskId: z.string() }, READ_ONLY, async ({ taskId }) => {
@@ -779,7 +821,7 @@ export function registerZiggsTools(server, creds, cfg) {
779
821
  return textResult({ task });
780
822
  }
781
823
  catch (e) {
782
- return toolError(e.message);
824
+ return toolError(e);
783
825
  }
784
826
  });
785
827
  // ---------------------------------------------------------------------------
@@ -15,7 +15,7 @@ const DEFAULT_WEB_URL = 'https://ziggsai.com';
15
15
  export function registerTrustTools(server, creds, cfg) {
16
16
  const webUrl = cfg?.ZIGGS_WEB_URL?.replace(/\/$/, '') ?? DEFAULT_WEB_URL;
17
17
  registerCapabilities(server, DISCOVERY_CAPABILITIES, creds);
18
- server.tool('ziggs_context_issue_grant', 'Issue bounded context access. Chat scope: admits agent via POST /chats/:id/members (agent-invite → pending_approval until humans consent) this works for you as a delegate. Agreement/org scope: issuing a NEW root grant is a human-authority action; if you are acting for a principal you are denied (AGENT_LACKS_HUMAN_AUTHORITY) instead use ziggs_context_delegate to hand a peer a narrower slice of a grant you already hold, or ask your human to issue it. Artifact scope: hands over one specific artifact and nothing else; it is always from-start (the artifact already exists), and if YOU authored it use ziggs_artifact_share instead — that needs no human authority. Defaults: from-now, narrow scope.', {
18
+ server.tool('ziggs_context_issue_grant', 'Issue bounded context access. ONE scope works for you as a delegate: chat, which admits the agent via POST /chats/:id/members (agent-invite → pending_approval until humans consent). Agreement, org AND artifact scope all mint a NEW root grant, which is a human-authority action acting for a principal you are denied (AGENT_LACKS_HUMAN_AUTHORITY) on all three alike, before the scope is even read. Your paths instead: ziggs_artifact_share for an artifact YOU authored (no human authority needed), ziggs_context_delegate to hand a peer a narrower slice of a grant you already hold, or ask your human to issue it. Artifact scope is always from-start (the artifact predates any watermark you could set) and refuses from-now. Defaults: from-now, narrow scope.', {
19
19
  holderId: z.string().describe('Bare agent id receiving the grant'),
20
20
  scopeKind: grantScopeKindSchema,
21
21
  scopeId: z.string().describe('chatId, agreementId, orgId, or artifactId'),
@@ -85,7 +85,7 @@ export function registerTrustTools(server, creds, cfg) {
85
85
  });
86
86
  }
87
87
  catch (e) {
88
- return toolError(e.message);
88
+ return toolError(e);
89
89
  }
90
90
  });
91
91
  registerCapability(server, contextDelegateCapability, creds);
@@ -104,7 +104,7 @@ export function registerTrustTools(server, creds, cfg) {
104
104
  return textResult({ grantId, ...result });
105
105
  }
106
106
  catch (e) {
107
- return toolError(e.message);
107
+ return toolError(e);
108
108
  }
109
109
  });
110
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/ziggs-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
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,12 +36,12 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@modelcontextprotocol/sdk": "^1.29.0",
39
- "@ziggs-ai/api-client": "^0.8.0",
39
+ "@ziggs-ai/api-client": "^0.9.0",
40
40
  "dotenv": "^16.6.1",
41
41
  "zod": "^3.24.2"
42
42
  },
43
43
  "devDependencies": {
44
- "@ziggs-ai/agent-sdk": "^0.9.0"
44
+ "@ziggs-ai/agent-sdk": "^0.10.0"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=20"
@@ -93,8 +93,8 @@ See [references/untrusted-input.md](references/untrusted-input.md).
93
93
 
94
94
  When coordinating with another org’s delegate:
95
95
 
96
- 1. Inbox → read new messages in the shared chat.
97
- 2. Reply with **`ziggs_chat_send`** or drive **`ziggs_agreement_propose`** / **`ziggs_agreement_respond`** as appropriate.
96
+ 1. Inbox → read new messages in the shared chat. Counterparties may appear as opaque `rpb_*` / `psn_*` presentation refs (plus a `presentation` face) — not as raw account ids.
97
+ 2. Reply with **`ziggs_chat_send`** (echo an `rpb_*` receiverId as-is; do not look it up, wake, or pay against it) or drive **`ziggs_agreement_propose`** / **`ziggs_agreement_respond`** as appropriate.
98
98
  3. If trust is missing, **`ziggs_agent_search`** → human picks counterparty → **`ziggs_context_issue_grant`** (with approval) before reading their context.
99
99
  4. Ack the handled envelope (`ackTo`) before ending the turn.
100
100