@ziggs-ai/ziggs-mcp 0.7.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilityAdapter.js +1 -1
- package/dist/inboxToolResult.d.ts +11 -2
- package/dist/inboxToolResult.js +95 -29
- package/dist/pendingDecisions.d.ts +26 -4
- package/dist/pendingDecisions.js +51 -25
- package/dist/toolError.d.ts +9 -3
- package/dist/toolError.js +48 -5
- package/dist/tools.js +84 -45
- package/dist/trustTools.js +3 -3
- package/package.json +3 -3
- package/skills/ziggs/SKILL.md +2 -2
|
@@ -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
|
|
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
|
|
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>;
|
package/dist/inboxToolResult.js
CHANGED
|
@@ -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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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,14 +238,22 @@ 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, {
|
|
256
|
+
const pending = formatPendingDecisionsPayload(inbox, origin, self, {
|
|
191
257
|
activeTasks,
|
|
192
258
|
activeTasksError,
|
|
193
259
|
withSessionCard: false,
|
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type InboxEnvelope, 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,7 +26,12 @@ export interface PendingDecisionItem {
|
|
|
9
26
|
proposedAt: string | null;
|
|
10
27
|
proposedAtLabel: string | null;
|
|
11
28
|
appUrl: string;
|
|
12
|
-
|
|
29
|
+
respondableBy: DecisionResponder;
|
|
30
|
+
/**
|
|
31
|
+
* The MCP tool call the agent runs on approval — null when the decision is
|
|
32
|
+
* the human's, because offering a tool that always 403s is what sent agents
|
|
33
|
+
* into retry loops.
|
|
34
|
+
*/
|
|
13
35
|
respondApprove: string | null;
|
|
14
36
|
respondReject: string | null;
|
|
15
37
|
sayApprove: string | null;
|
|
@@ -59,7 +81,7 @@ export declare function agreementsListAppUrl(origin: string): string;
|
|
|
59
81
|
export declare function connectionsSettingsAppUrl(origin: string): string;
|
|
60
82
|
/** Where the human decides paused transfers (ZIG-896). */
|
|
61
83
|
export declare function walletAppUrl(origin: string): string;
|
|
62
|
-
export declare function buildPendingDecisionItems(inbox: InboxEnvelope, webOrigin: string): PendingDecisionItem[];
|
|
84
|
+
export declare function buildPendingDecisionItems(inbox: InboxEnvelope, webOrigin: string, self: DecisionSelfIds): PendingDecisionItem[];
|
|
63
85
|
/** ZIG-896 — shape pending payment approvals for the session payload/card. */
|
|
64
86
|
export declare function buildPaymentApprovalItems(approvals: Array<Record<string, unknown>>, webOrigin: string): PaymentApprovalItem[];
|
|
65
87
|
/**
|
|
@@ -90,7 +112,7 @@ export declare function buildSessionChatCard(decisions: PendingDecisionItem[], w
|
|
|
90
112
|
paymentApprovals?: PaymentApprovalItem[];
|
|
91
113
|
}): string;
|
|
92
114
|
/** Structured session payload for MCP tools (ZIG-625 + active work). */
|
|
93
|
-
export declare function formatPendingDecisionsPayload(inbox: InboxEnvelope, webOrigin: string, opts?: {
|
|
115
|
+
export declare function formatPendingDecisionsPayload(inbox: InboxEnvelope, webOrigin: string, self: DecisionSelfIds, opts?: {
|
|
94
116
|
activeTasks?: Task[];
|
|
95
117
|
activeTasksError?: string;
|
|
96
118
|
/**
|
package/dist/pendingDecisions.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolvePendingApprovalPartyId, } from '@ziggs-ai/api-client';
|
|
1
2
|
const TITLE_MAX = 72;
|
|
2
3
|
const ACTIVE_TASK_LIMIT = 20;
|
|
3
4
|
/**
|
|
@@ -57,8 +58,12 @@ function planProgress(plan) {
|
|
|
57
58
|
const done = steps.filter((s) => s.status === 'completed' || s.status === 'skipped').length;
|
|
58
59
|
return { done, total };
|
|
59
60
|
}
|
|
60
|
-
function proposalToItem(p, origin) {
|
|
61
|
+
function proposalToItem(p, origin, self) {
|
|
61
62
|
const id = p.agreementId;
|
|
63
|
+
// The inbox lists proposals awaiting EITHER of our ids, so ask which one.
|
|
64
|
+
// Agent id first: when both owe a decision, ours is the one we can act on.
|
|
65
|
+
const slot = resolvePendingApprovalPartyId({ pendingPartyIds: p.pendingApprovalPartyIds ?? [], proposedTo: p.proposedTo }, [self.agentId, self.ownerUserId]);
|
|
66
|
+
const mine = slot != null && slot === self.agentId;
|
|
62
67
|
return {
|
|
63
68
|
kind: 'proposal',
|
|
64
69
|
agreementId: id,
|
|
@@ -67,51 +72,56 @@ function proposalToItem(p, origin) {
|
|
|
67
72
|
proposedAt: p.proposedAt,
|
|
68
73
|
proposedAtLabel: formatWhen(p.proposedAt),
|
|
69
74
|
appUrl: agreementAppUrl(origin, id),
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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,
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
82
|
function linkRequesterHeadline(c) {
|
|
77
|
-
// ZIG-1039: consent cards lead with a readable name + org
|
|
83
|
+
// ZIG-1039: consent cards lead with a readable name + org; ZIG-1137: ref is
|
|
84
|
+
// a non-addressable psn_* face, never an agent account id.
|
|
78
85
|
const name = c.requesterDisplayName?.trim();
|
|
79
86
|
const org = c.requesterOrgName?.trim();
|
|
80
87
|
if (name && org)
|
|
81
88
|
return `${name} (${org})`;
|
|
82
89
|
if (name)
|
|
83
90
|
return name;
|
|
84
|
-
return c.
|
|
91
|
+
return c.requesterRef;
|
|
85
92
|
}
|
|
86
|
-
function linkToItem(c, origin) {
|
|
93
|
+
function linkToItem(c, origin, self) {
|
|
87
94
|
const id = c.requestId;
|
|
88
95
|
const note = c.message?.trim() || null;
|
|
89
96
|
const headline = linkRequesterHeadline(c);
|
|
97
|
+
const slot = resolvePendingApprovalPartyId({ pendingPartyIds: c.pendingApprovalPartyIds ?? [], proposedTo: c.proposedTo }, [self.agentId, self.ownerUserId]);
|
|
98
|
+
const mine = slot != null && slot === self.agentId;
|
|
90
99
|
return {
|
|
91
100
|
kind: 'link_request',
|
|
92
101
|
agreementId: id,
|
|
93
102
|
title: truncateText(`Agent link · ${headline}`),
|
|
94
103
|
subtitle: note
|
|
95
104
|
? truncateText(note, 96)
|
|
96
|
-
: c.
|
|
97
|
-
? truncateText(`
|
|
105
|
+
: c.requesterRef && headline !== c.requesterRef
|
|
106
|
+
? truncateText(`ref: ${c.requesterRef}`, 96)
|
|
98
107
|
: null,
|
|
99
108
|
proposedAt: c.requestedAt,
|
|
100
109
|
proposedAtLabel: formatWhen(c.requestedAt),
|
|
101
110
|
appUrl: agreementAppUrl(origin, id),
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
respondableBy: mine ? 'agent' : 'human',
|
|
112
|
+
respondApprove: mine ? `ziggs_agreement_respond agreementId=${id} action=approve` : null,
|
|
113
|
+
respondReject: mine ? `ziggs_agreement_respond agreementId=${id} action=reject` : null,
|
|
114
|
+
sayApprove: mine ? `approve link ${id}` : null,
|
|
115
|
+
sayReject: mine ? `reject link ${id}` : null,
|
|
106
116
|
};
|
|
107
117
|
}
|
|
108
|
-
export function buildPendingDecisionItems(inbox, webOrigin) {
|
|
118
|
+
export function buildPendingDecisionItems(inbox, webOrigin, self) {
|
|
109
119
|
const items = [];
|
|
110
120
|
for (const p of inbox.proposalsAwaitingMe ?? []) {
|
|
111
|
-
items.push(proposalToItem(p, webOrigin));
|
|
121
|
+
items.push(proposalToItem(p, webOrigin, self));
|
|
112
122
|
}
|
|
113
123
|
for (const c of inbox.connectionRequestsAwaitingMe ?? []) {
|
|
114
|
-
items.push(linkToItem(c, webOrigin));
|
|
124
|
+
items.push(linkToItem(c, webOrigin, self));
|
|
115
125
|
}
|
|
116
126
|
return items;
|
|
117
127
|
}
|
|
@@ -216,14 +226,19 @@ function buildDecisionSection(items, opts) {
|
|
|
216
226
|
lines.push(`> ${item.subtitle}`);
|
|
217
227
|
}
|
|
218
228
|
lines.push('');
|
|
229
|
+
lines.push(`[Review in Ziggs →](${item.appUrl})`);
|
|
230
|
+
lines.push('');
|
|
219
231
|
if (item.sayApprove && item.respondApprove && item.sayReject && item.respondReject) {
|
|
220
|
-
lines.push(`[Review in Ziggs →](${item.appUrl})`);
|
|
221
|
-
lines.push('');
|
|
222
232
|
lines.push('| You say in chat | What the agent runs |');
|
|
223
233
|
lines.push('|:----------------|:--------------------|');
|
|
224
234
|
lines.push(`| \`${item.sayApprove}\` | \`${item.respondApprove}\` |`);
|
|
225
235
|
lines.push(`| \`${item.sayReject}\` | \`${item.respondReject}\` |`);
|
|
226
236
|
}
|
|
237
|
+
else {
|
|
238
|
+
// ZIG-1087 — this one is bound to the human's own party slot. Saying so
|
|
239
|
+
// beats offering a tool call that is refused every time.
|
|
240
|
+
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.');
|
|
241
|
+
}
|
|
227
242
|
lines.push('');
|
|
228
243
|
}
|
|
229
244
|
if (truncated > 0) {
|
|
@@ -356,9 +371,9 @@ export function buildSessionChatCard(decisions, work, opts) {
|
|
|
356
371
|
return lines.join('\n').trim();
|
|
357
372
|
}
|
|
358
373
|
/** Structured session payload for MCP tools (ZIG-625 + active work). */
|
|
359
|
-
export function formatPendingDecisionsPayload(inbox, webOrigin, opts) {
|
|
374
|
+
export function formatPendingDecisionsPayload(inbox, webOrigin, self, opts) {
|
|
360
375
|
const withSessionCard = opts?.withSessionCard !== false;
|
|
361
|
-
const decisions = buildPendingDecisionItems(inbox, webOrigin);
|
|
376
|
+
const decisions = buildPendingDecisionItems(inbox, webOrigin, self);
|
|
362
377
|
const activeWork = buildActiveWorkItems(opts?.activeTasks ?? [], webOrigin);
|
|
363
378
|
const paymentApprovals = buildPaymentApprovalItems(opts?.paymentApprovals ?? [], webOrigin);
|
|
364
379
|
const truncatedProposals = inbox.truncatedProposals ?? 0;
|
|
@@ -376,14 +391,25 @@ export function formatPendingDecisionsPayload(inbox, webOrigin, opts) {
|
|
|
376
391
|
agreementsListAppUrl: listUrl,
|
|
377
392
|
paymentApprovals,
|
|
378
393
|
};
|
|
379
|
-
|
|
394
|
+
// Only the card owner ships it (ZIG-659). ziggs_inbox and ziggs_auth_status
|
|
395
|
+
// pass withSessionCard: false, so building it there assembled every decision,
|
|
396
|
+
// payment and up-to-20 work sections just to drop them for a one-line hint.
|
|
397
|
+
const sessionChatCard = withSessionCard
|
|
398
|
+
? buildSessionChatCard(decisions, activeWork, cardOpts)
|
|
399
|
+
: null;
|
|
380
400
|
const proposalCount = decisions.filter((d) => d.kind === 'proposal').length;
|
|
381
401
|
const linkCount = decisions.filter((d) => d.kind === 'link_request').length;
|
|
402
|
+
// ZIG-1087 — decisions bound to the principal's own slot cannot be answered
|
|
403
|
+
// from this surface at all, so the instruction has to stop saying they can.
|
|
404
|
+
const humanOnly = decisions.filter((d) => d.respondableBy === 'human');
|
|
405
|
+
const humanOnlyNote = humanOnly.length
|
|
406
|
+
? ` ${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.`
|
|
407
|
+
: '';
|
|
382
408
|
const instruction = actionCount === 0
|
|
383
409
|
? 'No pending decisions or active tasks — continue with ziggs_inbox for scope news.'
|
|
384
410
|
: withSessionCard
|
|
385
|
-
?
|
|
386
|
-
: `Counts only here — ${SESSION_CARD_POINTER} Decisions: wait for explicit approve/reject before ziggs_agreement_respond
|
|
411
|
+
? `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.`
|
|
412
|
+
: `Counts only here — ${SESSION_CARD_POINTER} Decisions: wait for explicit approve/reject before ziggs_agreement_respond.${humanOnlyNote}`;
|
|
387
413
|
return {
|
|
388
414
|
pendingCount,
|
|
389
415
|
hasPending: pendingCount > 0,
|
|
@@ -448,7 +474,7 @@ export function buildPendingNextActions(decisions, work = []) {
|
|
|
448
474
|
actions.push(`${kindLabel(d.kind)} ${d.agreementId}: \`${d.sayApprove}\` or \`${d.sayReject}\``);
|
|
449
475
|
}
|
|
450
476
|
else {
|
|
451
|
-
actions.push(`${kindLabel(d.kind)} ${d.agreementId}:
|
|
477
|
+
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
478
|
}
|
|
453
479
|
}
|
|
454
480
|
for (const w of work.slice(0, 4)) {
|
package/dist/toolError.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
19
|
-
|
|
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
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
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
|
@@ -5,7 +5,7 @@ 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
|
|
@@ -98,6 +104,9 @@ async function loadSessionActionsPayload(creds, cfg, opts) {
|
|
|
98
104
|
paymentApprovalsError = e.message;
|
|
99
105
|
}
|
|
100
106
|
return formatPendingDecisionsPayload(inbox, webOrigin, {
|
|
107
|
+
agentId: creds.agentId,
|
|
108
|
+
ownerUserId: ownerPrincipalId(creds, cfg),
|
|
109
|
+
}, {
|
|
101
110
|
activeTasks,
|
|
102
111
|
activeTasksError,
|
|
103
112
|
paymentApprovals,
|
|
@@ -163,7 +172,7 @@ function registerMarketplaceTools(server, creds) {
|
|
|
163
172
|
});
|
|
164
173
|
}
|
|
165
174
|
catch (e) {
|
|
166
|
-
return toolError(e
|
|
175
|
+
return toolError(e);
|
|
167
176
|
}
|
|
168
177
|
});
|
|
169
178
|
}
|
|
@@ -177,7 +186,7 @@ function registerConnectionTools(server, creds) {
|
|
|
177
186
|
return textResult({ connections });
|
|
178
187
|
}
|
|
179
188
|
catch (e) {
|
|
180
|
-
return toolError(e
|
|
189
|
+
return toolError(e);
|
|
181
190
|
}
|
|
182
191
|
});
|
|
183
192
|
registerCapability(server, requestConnectionCapability, creds);
|
|
@@ -260,7 +269,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
260
269
|
return textResult({ count: orgs.length, orgs });
|
|
261
270
|
}
|
|
262
271
|
catch (e) {
|
|
263
|
-
return toolError(e
|
|
272
|
+
return toolError(e);
|
|
264
273
|
}
|
|
265
274
|
});
|
|
266
275
|
server.tool('ziggs_pending_decisions', ZIGGS_PENDING_DECISIONS_DESCRIPTION, {}, READ_ONLY, async () => {
|
|
@@ -277,7 +286,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
277
286
|
return textResult(payload);
|
|
278
287
|
}
|
|
279
288
|
catch (e) {
|
|
280
|
-
return toolError(e
|
|
289
|
+
return toolError(e);
|
|
281
290
|
}
|
|
282
291
|
});
|
|
283
292
|
if (cfg.debugTools) {
|
|
@@ -299,7 +308,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
299
308
|
});
|
|
300
309
|
}
|
|
301
310
|
catch (e) {
|
|
302
|
-
return toolError(e
|
|
311
|
+
return toolError(e);
|
|
303
312
|
}
|
|
304
313
|
});
|
|
305
314
|
}
|
|
@@ -320,7 +329,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
320
329
|
return textResult(result);
|
|
321
330
|
}
|
|
322
331
|
catch (e) {
|
|
323
|
-
return toolError(e
|
|
332
|
+
return toolError(e);
|
|
324
333
|
}
|
|
325
334
|
});
|
|
326
335
|
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 +350,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
341
350
|
return textResult({ count: agreements.length, scope: scope ?? 'mine', agreements });
|
|
342
351
|
}
|
|
343
352
|
catch (e) {
|
|
344
|
-
return toolError(e
|
|
353
|
+
return toolError(e);
|
|
345
354
|
}
|
|
346
355
|
});
|
|
347
356
|
server.tool('ziggs_agreement_get', 'Fetch a single agreement by id.', { agreementId: z.string() }, READ_ONLY, async ({ agreementId }) => {
|
|
@@ -352,7 +361,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
352
361
|
return textResult({ agreement });
|
|
353
362
|
}
|
|
354
363
|
catch (e) {
|
|
355
|
-
return toolError(e
|
|
364
|
+
return toolError(e);
|
|
356
365
|
}
|
|
357
366
|
});
|
|
358
367
|
server.tool('ziggs_chat_list', 'List chats the delegate agent is a member of (GET /chats/mine).', {}, READ_ONLY, async () => {
|
|
@@ -361,7 +370,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
361
370
|
return textResult({ count: chats.length, chats });
|
|
362
371
|
}
|
|
363
372
|
catch (e) {
|
|
364
|
-
return toolError(e
|
|
373
|
+
return toolError(e);
|
|
365
374
|
}
|
|
366
375
|
});
|
|
367
376
|
registerCapability(server, openConversationCapability, creds);
|
|
@@ -370,7 +379,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
370
379
|
receiverId: z
|
|
371
380
|
.string()
|
|
372
381
|
.optional()
|
|
373
|
-
.describe("
|
|
382
|
+
.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
383
|
text: z.string(),
|
|
375
384
|
entryType: z
|
|
376
385
|
.string()
|
|
@@ -398,7 +407,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
398
407
|
return textResult(result);
|
|
399
408
|
}
|
|
400
409
|
catch (e) {
|
|
401
|
-
return toolError(e
|
|
410
|
+
return toolError(e);
|
|
402
411
|
}
|
|
403
412
|
});
|
|
404
413
|
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 +426,10 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
417
426
|
price: z
|
|
418
427
|
.number()
|
|
419
428
|
.optional()
|
|
420
|
-
.describe('Amount in CENTS — 500 means $5.00. Optional; does not trigger a
|
|
421
|
-
'
|
|
429
|
+
.describe('Amount in CENTS — 500 means $5.00, and ϟ5.00 in the UI. Optional; does not trigger a ' +
|
|
430
|
+
'transfer by itself. Convert BOTH ways or you are off by 100x: a human who says ' +
|
|
431
|
+
'"ϟ2" or "$2 per task" means price 200, not 2; quoting 500 back as "$500" or "ϟ500" ' +
|
|
432
|
+
'is the same mistake inverted. ϟ is the currency symbol the UI shows — it is not cents.'),
|
|
422
433
|
engagementKind: z
|
|
423
434
|
.enum(['hire', 'service', 'link'])
|
|
424
435
|
.optional()
|
|
@@ -477,7 +488,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
477
488
|
});
|
|
478
489
|
}
|
|
479
490
|
catch (e) {
|
|
480
|
-
return toolError(e
|
|
491
|
+
return toolError(e);
|
|
481
492
|
}
|
|
482
493
|
});
|
|
483
494
|
registerCapability(server, agreementClaimCapability, creds);
|
|
@@ -486,7 +497,6 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
486
497
|
executorId: z.string().describe('Agent doing the delegated work'),
|
|
487
498
|
chatId: z.string().describe('Chat the delegation is coordinated in'),
|
|
488
499
|
description: z.string().describe('What the sub-agreement covers'),
|
|
489
|
-
parentTaskId: z.string().optional(),
|
|
490
500
|
price: z
|
|
491
501
|
.number()
|
|
492
502
|
.optional()
|
|
@@ -498,14 +508,13 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
498
508
|
.optional()
|
|
499
509
|
.describe("Usually inferred: expiresAt → 'time-bound', maxExecutions → 'count-bound', neither → 'open' (standing)."),
|
|
500
510
|
agreementDescription: z.string().optional(),
|
|
501
|
-
}, WRITE, async ({ parentAgreementId, executorId, chatId, description,
|
|
511
|
+
}, WRITE, async ({ parentAgreementId, executorId, chatId, description, price, expiresAt, maxExecutions, lifecycle, agreementDescription, }) => {
|
|
502
512
|
try {
|
|
503
513
|
const agreement = await delegateAgreement({
|
|
504
514
|
parentAgreementId,
|
|
505
515
|
executorId,
|
|
506
516
|
chatId,
|
|
507
517
|
description,
|
|
508
|
-
parentTaskId,
|
|
509
518
|
price,
|
|
510
519
|
expiresAt,
|
|
511
520
|
maxExecutions,
|
|
@@ -515,13 +524,13 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
515
524
|
return textResult({ agreement });
|
|
516
525
|
}
|
|
517
526
|
catch (e) {
|
|
518
|
-
return toolError(e
|
|
527
|
+
return toolError(e);
|
|
519
528
|
}
|
|
520
529
|
});
|
|
521
530
|
if (!cfg.coreOnly) {
|
|
522
531
|
registerMarketplaceTools(server, creds);
|
|
523
532
|
}
|
|
524
|
-
server.tool('ziggs_agreement_respond', 'Approve or reject a pending DIRECT agreement proposal addressed to
|
|
533
|
+
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
534
|
agreementId: z.string(),
|
|
526
535
|
action: z.enum(['approve', 'reject']),
|
|
527
536
|
}, WRITE, async ({ agreementId, action }) => {
|
|
@@ -530,13 +539,15 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
530
539
|
const ownerId = claims?.ownerId ?? cfg.ZIGGS_OWNER_USER_ID;
|
|
531
540
|
const updated = await respondToAgreement(agreementId, action, creds, {
|
|
532
541
|
ownerUserId: ownerId,
|
|
542
|
+
// So a refusal can name where the human goes, not just why.
|
|
543
|
+
appUrl: agreementAppUrl(resolveWebAppOrigin(cfg.ZIGGS_WEB_URL), agreementId),
|
|
533
544
|
});
|
|
534
545
|
// ZIG-957: link approvals/rejects share the same summary shape as claim/revoke.
|
|
535
546
|
const agreement = updated?.engagementKind === 'link' ? linkSummary(updated) : updated;
|
|
536
547
|
return textResult({ agreement });
|
|
537
548
|
}
|
|
538
549
|
catch (e) {
|
|
539
|
-
return toolError(e
|
|
550
|
+
return toolError(e);
|
|
540
551
|
}
|
|
541
552
|
});
|
|
542
553
|
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.', {
|
|
@@ -559,12 +570,15 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
559
570
|
});
|
|
560
571
|
}
|
|
561
572
|
catch (e) {
|
|
562
|
-
return toolError(e
|
|
573
|
+
return toolError(e);
|
|
563
574
|
}
|
|
564
575
|
});
|
|
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,
|
|
576
|
+
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
577
|
agreementId: z.string().describe('The pending agreement to counter'),
|
|
567
|
-
price: z
|
|
578
|
+
price: z
|
|
579
|
+
.number()
|
|
580
|
+
.optional()
|
|
581
|
+
.describe('Revised price, in CENTS — 500 means $5.00 / ϟ5.00 (see ziggs_agreement_propose).'),
|
|
568
582
|
agreementDescription: z
|
|
569
583
|
.string()
|
|
570
584
|
.optional()
|
|
@@ -579,22 +593,13 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
579
593
|
.string()
|
|
580
594
|
.optional()
|
|
581
595
|
.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
596
|
}, WRITE, async ({ agreementId, ...counter }) => {
|
|
592
597
|
try {
|
|
593
598
|
const agreement = await counterAgreement(agreementId, counter, creds);
|
|
594
599
|
return textResult({ status: 'countered', agreementId, agreement });
|
|
595
600
|
}
|
|
596
601
|
catch (e) {
|
|
597
|
-
return toolError(e
|
|
602
|
+
return toolError(e);
|
|
598
603
|
}
|
|
599
604
|
});
|
|
600
605
|
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 +610,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
605
610
|
return textResult({ status: 'fulfilled', agreementId, agreement: result.agreement });
|
|
606
611
|
}
|
|
607
612
|
catch (e) {
|
|
608
|
-
return toolError(e
|
|
613
|
+
return toolError(e);
|
|
609
614
|
}
|
|
610
615
|
});
|
|
611
616
|
server.tool('ziggs_inbox', ZIGGS_INBOX_DESCRIPTION, {
|
|
@@ -648,10 +653,10 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
648
653
|
catch {
|
|
649
654
|
// omit grant tags when the grants read fails
|
|
650
655
|
}
|
|
651
|
-
return textResult(formatInboxToolResult(inbox, acked, cfg.ZIGGS_WEB_URL, activeTasks, reach, activeTasksError));
|
|
656
|
+
return textResult(formatInboxToolResult(inbox, acked, cfg.ZIGGS_WEB_URL, activeTasks, reach, activeTasksError, { agentId: creds.agentId, ownerUserId: ownerPrincipalId(creds, cfg) }));
|
|
652
657
|
}
|
|
653
658
|
catch (e) {
|
|
654
|
-
return toolError(e
|
|
659
|
+
return toolError(e);
|
|
655
660
|
}
|
|
656
661
|
});
|
|
657
662
|
registerCapabilities(server, GRANTS_CAPABILITIES, creds);
|
|
@@ -681,7 +686,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
681
686
|
// ---------------------------------------------------------------------------
|
|
682
687
|
// Task mutation tools (ZIG-555)
|
|
683
688
|
// ---------------------------------------------------------------------------
|
|
684
|
-
server.tool('ziggs_task_create', 'Create a task under an agreement. Every task belongs to exactly one agreement (agreementId required).', {
|
|
689
|
+
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
690
|
agreementId: z.string().describe('Agreement this task belongs to'),
|
|
686
691
|
description: z.string().describe('What the task entails'),
|
|
687
692
|
parentTaskId: z.string().optional().describe('Parent task id for sub-tasks'),
|
|
@@ -693,13 +698,47 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
693
698
|
.array(z.string())
|
|
694
699
|
.optional()
|
|
695
700
|
.describe('Artifact ids this task consumes as structured inputs — pass prior-step output handles without embedding them in description'),
|
|
696
|
-
|
|
701
|
+
// ZIG-1096: POST /tasks and the SDK's task_create have always taken
|
|
702
|
+
// these three; only this surface hid them, so an agent on MCP had to
|
|
703
|
+
// create-then-replace_plan even when it already knew the steps.
|
|
704
|
+
plan: z
|
|
705
|
+
.object({
|
|
706
|
+
steps: z
|
|
707
|
+
.array(z.object({
|
|
708
|
+
stepId: z.string().describe('Stable id for this step (e.g. uuid)'),
|
|
709
|
+
description: z
|
|
710
|
+
.string()
|
|
711
|
+
.describe('What this step does, in one line — required and non-blank'),
|
|
712
|
+
order: z.number().int(),
|
|
713
|
+
}))
|
|
714
|
+
.describe('Ordered steps the task starts with'),
|
|
715
|
+
})
|
|
716
|
+
.optional()
|
|
717
|
+
.describe('The plan the task is born with. Omit to start without one.'),
|
|
718
|
+
planReviewTiming: z
|
|
719
|
+
.enum(['with_proposal', 'before_execution'])
|
|
720
|
+
.optional()
|
|
721
|
+
.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.'),
|
|
722
|
+
requireMidWorkPlanAck: z
|
|
723
|
+
.boolean()
|
|
724
|
+
.optional()
|
|
725
|
+
.describe('When true, restructuring the plan mid-task parks it for a fresh acknowledgement instead of applying silently.'),
|
|
726
|
+
}, WRITE, async ({ agreementId, description, parentTaskId, assigneeId, inputArtifactIds, plan, planReviewTiming, requireMidWorkPlanAck, }) => {
|
|
697
727
|
try {
|
|
698
|
-
const task = await createTask({
|
|
728
|
+
const task = await createTask({
|
|
729
|
+
agreementId,
|
|
730
|
+
description,
|
|
731
|
+
parentTaskId,
|
|
732
|
+
assigneeId,
|
|
733
|
+
inputArtifactIds,
|
|
734
|
+
plan,
|
|
735
|
+
planReviewTiming,
|
|
736
|
+
requireMidWorkPlanAck,
|
|
737
|
+
}, creds);
|
|
699
738
|
return textResult({ ok: true, task });
|
|
700
739
|
}
|
|
701
740
|
catch (e) {
|
|
702
|
-
return toolError(e
|
|
741
|
+
return toolError(e);
|
|
703
742
|
}
|
|
704
743
|
});
|
|
705
744
|
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 +764,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
725
764
|
return textResult({ ok: true, task });
|
|
726
765
|
}
|
|
727
766
|
catch (e) {
|
|
728
|
-
return toolError(e
|
|
767
|
+
return toolError(e);
|
|
729
768
|
}
|
|
730
769
|
});
|
|
731
770
|
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 +782,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
743
782
|
return textResult({ ok: true, task });
|
|
744
783
|
}
|
|
745
784
|
catch (e) {
|
|
746
|
-
return toolError(e
|
|
785
|
+
return toolError(e);
|
|
747
786
|
}
|
|
748
787
|
});
|
|
749
788
|
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 +807,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
768
807
|
return textResult(result);
|
|
769
808
|
}
|
|
770
809
|
catch (e) {
|
|
771
|
-
return toolError(e
|
|
810
|
+
return toolError(e);
|
|
772
811
|
}
|
|
773
812
|
});
|
|
774
813
|
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 +818,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
779
818
|
return textResult({ task });
|
|
780
819
|
}
|
|
781
820
|
catch (e) {
|
|
782
|
-
return toolError(e
|
|
821
|
+
return toolError(e);
|
|
783
822
|
}
|
|
784
823
|
});
|
|
785
824
|
// ---------------------------------------------------------------------------
|
package/dist/trustTools.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.9.0",
|
|
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.
|
|
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.
|
|
44
|
+
"@ziggs-ai/agent-sdk": "^0.10.0"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=20"
|
package/skills/ziggs/SKILL.md
CHANGED
|
@@ -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
|
|