@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.
- package/dist/inboxToolResult.js +3 -2
- package/dist/pendingDecisions.d.ts +27 -9
- package/dist/pendingDecisions.js +85 -39
- package/dist/toolError.d.ts +10 -10
- package/dist/toolError.js +45 -53
- package/dist/tools.js +61 -58
- package/package.json +2 -2
package/dist/inboxToolResult.js
CHANGED
|
@@ -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
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
|
|
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
|
/**
|
package/dist/pendingDecisions.js
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
// The inbox lists
|
|
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({
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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(`| \`${
|
|
235
|
-
lines.push(`| \`${
|
|
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 =
|
|
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
|
-
|
|
474
|
-
|
|
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.`);
|
package/dist/toolError.d.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ZIG-667 — one error surface for every MCP tool.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
/**
|
|
17
|
-
|
|
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
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
62
|
-
if (typeof err
|
|
63
|
-
return null;
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
78
|
+
return { code: 'TOOL_ERROR', message };
|
|
80
79
|
}
|
|
81
80
|
const code = codeForStatus(status);
|
|
82
|
-
//
|
|
83
|
-
|
|
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
|
-
//
|
|
95
|
-
// code
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
101
|
-
paymentApprovals =
|
|
110
|
+
if (paymentsSettled.status === 'fulfilled') {
|
|
111
|
+
paymentApprovals = paymentsSettled.value;
|
|
102
112
|
}
|
|
103
|
-
|
|
104
|
-
paymentApprovalsError =
|
|
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
|
-
|
|
204
|
-
const access =
|
|
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
|
-
|
|
213
|
-
switchOrgHint = `Could not load runtime org: ${
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
39
|
+
"@ziggs-ai/api-client": "^0.9.1",
|
|
40
40
|
"dotenv": "^16.6.1",
|
|
41
41
|
"zod": "^3.24.2"
|
|
42
42
|
},
|