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