@ziggs-ai/ziggs-mcp 0.1.34 → 0.1.35
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/README.md +7 -4
- package/dist/paymentTools.d.ts +13 -0
- package/dist/paymentTools.js +252 -0
- package/dist/pendingDecisions.d.ts +28 -0
- package/dist/pendingDecisions.js +84 -3
- package/dist/toolError.js +1 -1
- package/dist/tools.js +108 -160
- package/dist/trustTools.js +16 -0
- package/examples/claude-code.md +1 -1
- package/package.json +5 -2
- package/skills/ziggs/SKILL.md +1 -1
- package/skills/ziggs/references/grants-and-approvals.md +9 -2
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
MCP (stdio) server for **Claude Code**, **Cursor**, and other MCP hosts.
|
|
4
4
|
|
|
5
|
-
**In scope:** chat, agreements (service and hire, direct or published), scope, context discovery/reads, artifacts.
|
|
6
|
-
|
|
5
|
+
**In scope:** chat, agreements (service and hire, direct or published), scope, context discovery/reads, artifacts, payments (`ziggs_payment_*` — transfers, escrow holds, payment grants; ZIG-896).
|
|
6
|
+
Transfers above the wallet owner's policy pause as `approval_required` — the human decides on the wallet page (surfaced by `ziggs_pending_decisions`); there is no agent-side approve tool.
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -41,7 +41,7 @@ Skill only (no plugin): `skills/ziggs/SKILL.md` ships in the package for org pro
|
|
|
41
41
|
|
|
42
42
|
| Step | Tool |
|
|
43
43
|
|------|------|
|
|
44
|
-
| List chats / discover reach | `ziggs_list_chats` or `
|
|
44
|
+
| List chats / discover reach | `ziggs_list_chats` or `ziggs_list_grants` |
|
|
45
45
|
| Send message | `ziggs_send_message` |
|
|
46
46
|
| Propose + respond | `ziggs_propose_agreement`, `ziggs_respond_to_agreement` |
|
|
47
47
|
|
|
@@ -194,10 +194,11 @@ OP_KEY_A=... AGENT_A=... USER_B=... OP_KEY_B=... AGENT_B=... \
|
|
|
194
194
|
| Tool | Maps to |
|
|
195
195
|
|------|---------|
|
|
196
196
|
| `ziggs_inbox` | `GET /inbox` + `POST /inbox/ack` |
|
|
197
|
-
| `
|
|
197
|
+
| `ziggs_list_grants` | `GET /grants` (all rails) |
|
|
198
198
|
| `ziggs_read_context` | `GET /context/read/:type` |
|
|
199
199
|
| `ziggs_record_artifact` | `POST /artifacts` |
|
|
200
200
|
| `ziggs_search_agents` | Agent search |
|
|
201
|
+
| `ziggs_get_agent` | `GET /agents/:id` — full profile of one agent by exact id |
|
|
201
202
|
| `ziggs_issue_grant` | Chat admission or `POST /context/grants` |
|
|
202
203
|
| `ziggs_delegate_grant` | `POST /context/grants/:id/delegate` |
|
|
203
204
|
| `ziggs_revoke_grant` | `DELETE /context/grants/:id` |
|
|
@@ -214,6 +215,8 @@ OP_KEY_A=... AGENT_A=... USER_B=... OP_KEY_B=... AGENT_B=... \
|
|
|
214
215
|
| `ziggs_send_message` | `POST /chats/:id/messages` |
|
|
215
216
|
| `ziggs_propose_agreement` | `POST /agreements/proposals` |
|
|
216
217
|
| `ziggs_respond_to_agreement` | `PUT /agreements/:id/approvals/:partyId` (owner principal; approves hire, service, and `link` proposals) |
|
|
218
|
+
| `ziggs_counter_agreement` | `POST /agreements/:id/counter` — counter a pending proposal with revised terms |
|
|
219
|
+
| `ziggs_fulfill_agreement` | `POST /agreements/:id/fulfill` — provider marks its agreement complete |
|
|
217
220
|
|
|
218
221
|
---
|
|
219
222
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { type Creds } from '@ziggs-ai/api-client';
|
|
3
|
+
/**
|
|
4
|
+
* ZIG-896 — the wallet toolset on the MCP surface, same base names as the SDK's
|
|
5
|
+
* PAYMENT_TOOLS (payment_* → ziggs_payment_*), riding the shared PaymentsClient
|
|
6
|
+
* (ZIG-894). Safety is unchanged: money-moving calls are policy-gated
|
|
7
|
+
* server-side (transfers above threshold return `approval_required`; the human
|
|
8
|
+
* decides on the wallet page — surfaced by ziggs_pending_decisions), and rails
|
|
9
|
+
* stay gated by the operator key's payments scopes. There is deliberately NO
|
|
10
|
+
* approve/decide tool on either agent surface — an agent must not approve its
|
|
11
|
+
* own spend.
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerPaymentTools(server: McpServer, creds: Creds): void;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { PaymentsClient } from '@ziggs-ai/api-client';
|
|
3
|
+
import { READ_ONLY, WRITE, DESTRUCTIVE } from './toolAnnotations.js';
|
|
4
|
+
import { toolError } from './toolError.js';
|
|
5
|
+
function textResult(data) {
|
|
6
|
+
return {
|
|
7
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function buildCaveats(args) {
|
|
11
|
+
const caveats = [];
|
|
12
|
+
if (args.maxAmount != null)
|
|
13
|
+
caveats.push({ type: 'max_amount', value: args.maxAmount });
|
|
14
|
+
if (args.dailyBudget != null)
|
|
15
|
+
caveats.push({ type: 'daily_budget', value: args.dailyBudget });
|
|
16
|
+
if (args.allowedRecipients != null)
|
|
17
|
+
caveats.push({ type: 'allowed_recipients', value: args.allowedRecipients });
|
|
18
|
+
if (args.expiresInSeconds != null)
|
|
19
|
+
caveats.push({ type: 'expires_at', value: Date.now() + args.expiresInSeconds * 1000 });
|
|
20
|
+
return caveats;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* ZIG-896 — the wallet toolset on the MCP surface, same base names as the SDK's
|
|
24
|
+
* PAYMENT_TOOLS (payment_* → ziggs_payment_*), riding the shared PaymentsClient
|
|
25
|
+
* (ZIG-894). Safety is unchanged: money-moving calls are policy-gated
|
|
26
|
+
* server-side (transfers above threshold return `approval_required`; the human
|
|
27
|
+
* decides on the wallet page — surfaced by ziggs_pending_decisions), and rails
|
|
28
|
+
* stay gated by the operator key's payments scopes. There is deliberately NO
|
|
29
|
+
* approve/decide tool on either agent surface — an agent must not approve its
|
|
30
|
+
* own spend.
|
|
31
|
+
*/
|
|
32
|
+
export function registerPaymentTools(server, creds) {
|
|
33
|
+
const client = () => new PaymentsClient(creds.operatorKey, creds.agentId);
|
|
34
|
+
server.tool('ziggs_payment_balance', "Check the caller's current wallet balance and available balance (total minus active holds). Use before a transfer to confirm sufficient funds.", {}, READ_ONLY, async () => {
|
|
35
|
+
try {
|
|
36
|
+
return textResult(await client().balance());
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return toolError(e.message);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
server.tool('ziggs_payment_resolve_wallet', 'Look up a walletId by userId or agentId. Use before a transfer when you only know the recipient by their platform ID.', {
|
|
43
|
+
userId: z.string().optional().describe('User to resolve'),
|
|
44
|
+
agentId: z.string().optional().describe('Agent to resolve'),
|
|
45
|
+
}, READ_ONLY, async ({ userId, agentId }) => {
|
|
46
|
+
if (!userId && !agentId) {
|
|
47
|
+
return toolError('Provide userId or agentId to resolve a wallet');
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const wallet = (await client().resolve({ userId, agentId }));
|
|
51
|
+
return textResult({
|
|
52
|
+
walletId: wallet?.['walletId'] || null,
|
|
53
|
+
ownerId: wallet?.['ownerId'] || null,
|
|
54
|
+
currency: wallet?.['currency'] || 'pez',
|
|
55
|
+
status: wallet?.['status'] || null,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
return toolError(e.message);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
server.tool('ziggs_payment_transfer', 'Transfer funds to another wallet. Amounts are integer cents. As a delegate you spend under a payment grant the wallet owner issued (paymentGrantId — find yours via ziggs_list_grants scopeKind=wallet). Transfers above the owner\'s policy pause with status "approval_required": the human approves on the wallet page (it also shows in ziggs_pending_decisions) — you can wait inline with ziggs_payment_wait_for_approval, and you must NEVER approve your own transfer.', {
|
|
63
|
+
toWalletId: z
|
|
64
|
+
.string()
|
|
65
|
+
.describe('Destination wal_... id — or a userId/agentId to auto-resolve'),
|
|
66
|
+
amount: z.number().describe('Amount in integer cents, > 0'),
|
|
67
|
+
description: z.string().optional().describe('Human-readable transfer memo'),
|
|
68
|
+
idempotencyKey: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe('Client-supplied key to make retries safe (auto-generated when omitted)'),
|
|
72
|
+
paymentGrantId: z
|
|
73
|
+
.string()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe('Payment grant to spend under (required for agent-impersonated transfers)'),
|
|
76
|
+
}, WRITE, async ({ toWalletId, amount, description, idempotencyKey, paymentGrantId }) => {
|
|
77
|
+
if (!amount || amount <= 0)
|
|
78
|
+
return toolError('amount must be positive');
|
|
79
|
+
try {
|
|
80
|
+
const result = (await client().transfer({
|
|
81
|
+
to: toWalletId,
|
|
82
|
+
amount: Math.round(amount),
|
|
83
|
+
description: description || 'Agent-initiated transfer',
|
|
84
|
+
idempotencyKey,
|
|
85
|
+
paymentGrantId,
|
|
86
|
+
}));
|
|
87
|
+
if (result['status'] === 'approval_required') {
|
|
88
|
+
return textResult({
|
|
89
|
+
status: 'approval_required',
|
|
90
|
+
approvalId: result['approvalId'] || null,
|
|
91
|
+
expiresAt: result['expiresAt'] || null,
|
|
92
|
+
reason: result['reason'] || null,
|
|
93
|
+
amount,
|
|
94
|
+
toWalletId: result['toWalletId'],
|
|
95
|
+
note: 'Transfer paused: the wallet owner must approve this amount on the wallet page (also listed by ziggs_pending_decisions). Tell the human now (pull-only MCP has no push). ' +
|
|
96
|
+
'Wait inline with ziggs_payment_wait_for_approval when you expect a quick decision (≤2 min).',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return textResult({
|
|
100
|
+
status: 'transferred',
|
|
101
|
+
transactionId: result['transactionId'] || null,
|
|
102
|
+
amount,
|
|
103
|
+
toWalletId: result['toWalletId'],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
return toolError(e.message);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
server.tool('ziggs_payment_wait_for_approval', 'Poll a paused transfer (status "approval_required") until the human decides or the timeout passes. Returns executed | rejected | expired | timeout | gone. Use for quick decisions (≤2 min); for longer waits, stop and check again next session.', {
|
|
111
|
+
approvalId: z.string().describe('Approval to wait on (from ziggs_payment_transfer)'),
|
|
112
|
+
timeoutMs: z.number().optional().describe('Max wait, default 120000'),
|
|
113
|
+
pollMs: z.number().optional().describe('Poll interval, default 3000 (min 500)'),
|
|
114
|
+
}, READ_ONLY, async ({ approvalId, timeoutMs, pollMs }) => {
|
|
115
|
+
try {
|
|
116
|
+
const result = (await client().waitForApproval(approvalId, {
|
|
117
|
+
timeoutMs,
|
|
118
|
+
pollMs,
|
|
119
|
+
}));
|
|
120
|
+
return textResult({
|
|
121
|
+
status: result['status'],
|
|
122
|
+
approvalId,
|
|
123
|
+
transactionId: result['transactionId'] || null,
|
|
124
|
+
approval: result['approval'] || null,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
return toolError(e.message);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
server.tool('ziggs_payment_hold', "Pre-authorize (escrow) funds without moving them. Use to reserve payment at agreement formation; release with ziggs_payment_release once work is complete, or refund if it's cancelled.", {
|
|
132
|
+
amount: z.number().describe('Amount in integer cents, > 0'),
|
|
133
|
+
description: z.string().optional().describe('Human-readable hold memo'),
|
|
134
|
+
idempotencyKey: z.string().optional().describe('Client-supplied retry-safety key'),
|
|
135
|
+
}, WRITE, async ({ amount, description, idempotencyKey }) => {
|
|
136
|
+
if (!amount || amount <= 0)
|
|
137
|
+
return toolError('amount must be positive');
|
|
138
|
+
try {
|
|
139
|
+
const result = (await client().hold({
|
|
140
|
+
amount: Math.round(amount),
|
|
141
|
+
description: description || 'Agent escrow hold',
|
|
142
|
+
idempotencyKey,
|
|
143
|
+
}));
|
|
144
|
+
return textResult({
|
|
145
|
+
status: 'held',
|
|
146
|
+
transactionId: result['transaction']?.['transactionId'] || null,
|
|
147
|
+
amount,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
return toolError(e.message);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
server.tool('ziggs_payment_release', "Settle or refund an escrow hold. action='complete' transfers held funds to toWalletId (work done); action='refund' returns funds to the sender (work cancelled).", {
|
|
155
|
+
holdId: z.string().describe('Hold to settle (transactionId from ziggs_payment_hold)'),
|
|
156
|
+
action: z.enum(['complete', 'refund']).describe('complete = pay out, refund = return'),
|
|
157
|
+
toWalletId: z
|
|
158
|
+
.string()
|
|
159
|
+
.optional()
|
|
160
|
+
.describe('Destination wallet — required when action=complete'),
|
|
161
|
+
idempotencyKey: z.string().optional().describe('Client-supplied retry-safety key'),
|
|
162
|
+
}, WRITE, async ({ holdId, action, toWalletId, idempotencyKey }) => {
|
|
163
|
+
if (action === 'complete' && !toWalletId) {
|
|
164
|
+
return toolError('toWalletId is required when action=complete');
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const result = (await client().release({
|
|
168
|
+
holdId,
|
|
169
|
+
action,
|
|
170
|
+
toWalletId,
|
|
171
|
+
idempotencyKey,
|
|
172
|
+
}));
|
|
173
|
+
return textResult({
|
|
174
|
+
status: action === 'complete' ? 'settled' : 'refunded',
|
|
175
|
+
transactionId: result['transaction']?.['transactionId'] || null,
|
|
176
|
+
holdId,
|
|
177
|
+
action,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
return toolError(e.message);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
const grantCaveatArgs = {
|
|
185
|
+
maxAmount: z.number().optional().describe('Per-transfer ceiling in cents'),
|
|
186
|
+
dailyBudget: z.number().optional().describe('Rolling daily budget in cents'),
|
|
187
|
+
allowedRecipients: z
|
|
188
|
+
.array(z.string())
|
|
189
|
+
.optional()
|
|
190
|
+
.describe('Wallet ids the holder may pay'),
|
|
191
|
+
expiresInSeconds: z.number().optional().describe('Grant lifetime from now'),
|
|
192
|
+
};
|
|
193
|
+
server.tool('ziggs_payment_issue_grant', "Issue a payment grant delegating bounded spend from the operator's wallet to an agent holder. Caveats bound what the holder can do (max_amount, daily_budget, allowed_recipients, expiry). The holder spends by passing the grantId as paymentGrantId on transfers.", {
|
|
194
|
+
holderId: z.string().describe('Agent that will hold the grant'),
|
|
195
|
+
...grantCaveatArgs,
|
|
196
|
+
}, WRITE, async ({ holderId, ...caveatArgs }) => {
|
|
197
|
+
try {
|
|
198
|
+
const caveats = buildCaveats(caveatArgs);
|
|
199
|
+
const result = (await client().issueGrant({ holderId, caveats }));
|
|
200
|
+
const grant = result['grant'];
|
|
201
|
+
return textResult({
|
|
202
|
+
grantId: grant?.['grantId'] || null,
|
|
203
|
+
holderId: grant?.['holderId'] || holderId,
|
|
204
|
+
caveats: grant?.['caveats'] || caveats,
|
|
205
|
+
expiresAt: grant?.['expiresAt'] || null,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
return toolError(e.message);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
server.tool('ziggs_payment_attenuate_grant', 'Re-delegate a payment grant you hold to another agent with TIGHTER caveats (narrowing only — the child can never exceed the parent). Use to pass a bounded spend slice to a sub-agent.', {
|
|
213
|
+
grantId: z.string().describe('Parent grant to attenuate'),
|
|
214
|
+
holderId: z.string().describe('Agent that will hold the narrowed grant'),
|
|
215
|
+
...grantCaveatArgs,
|
|
216
|
+
}, WRITE, async ({ grantId, holderId, ...caveatArgs }) => {
|
|
217
|
+
try {
|
|
218
|
+
const caveats = buildCaveats(caveatArgs);
|
|
219
|
+
const result = (await client().attenuateGrant({
|
|
220
|
+
grantId,
|
|
221
|
+
holderId,
|
|
222
|
+
caveats,
|
|
223
|
+
}));
|
|
224
|
+
const grant = result['grant'];
|
|
225
|
+
return textResult({
|
|
226
|
+
grantId: grant?.['grantId'] || null,
|
|
227
|
+
parentGrantId: grant?.['parentGrantId'] || grantId,
|
|
228
|
+
holderId: grant?.['holderId'] || holderId,
|
|
229
|
+
caveats: grant?.['caveats'] || caveats,
|
|
230
|
+
expiresAt: grant?.['expiresAt'] || null,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
return toolError(e.message);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
server.tool('ziggs_payment_revoke_grant', 'Revoke a payment grant (and its attenuated children). The holder can no longer spend under it.', {
|
|
238
|
+
grantId: z.string().describe('Grant to revoke'),
|
|
239
|
+
}, DESTRUCTIVE, async ({ grantId }) => {
|
|
240
|
+
try {
|
|
241
|
+
const result = (await client().revokeGrant(grantId));
|
|
242
|
+
return textResult({
|
|
243
|
+
status: 'revoked',
|
|
244
|
+
grantId,
|
|
245
|
+
revoked: result?.['revoked'] ?? null,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
return toolError(e.message);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
@@ -25,6 +25,26 @@ export interface ActiveWorkItem {
|
|
|
25
25
|
appUrl: string | null;
|
|
26
26
|
sayWork: string;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* ZIG-896 — a paused transfer awaiting the wallet owner's decision
|
|
30
|
+
* (GET /payments/approvals?status=pending). Deliberately NOT a
|
|
31
|
+
* PendingDecisionItem: there is no agent-side approve tool on any surface —
|
|
32
|
+
* the human decides on the wallet page. The agent may only poll with
|
|
33
|
+
* ziggs_payment_wait_for_approval.
|
|
34
|
+
*/
|
|
35
|
+
export interface PaymentApprovalItem {
|
|
36
|
+
approvalId: string;
|
|
37
|
+
amount: number | null;
|
|
38
|
+
toWalletId: string | null;
|
|
39
|
+
description: string | null;
|
|
40
|
+
reason: string | null;
|
|
41
|
+
initiatorId: string | null;
|
|
42
|
+
requestedAt: string | null;
|
|
43
|
+
requestedAtLabel: string | null;
|
|
44
|
+
expiresAt: string | null;
|
|
45
|
+
appUrl: string;
|
|
46
|
+
waitTool: string;
|
|
47
|
+
}
|
|
28
48
|
/**
|
|
29
49
|
* ZIG-659: pointer emitted by the tools that do NOT own the session card
|
|
30
50
|
* (ziggs_auth_status, ziggs_inbox). They report the counts and send the caller
|
|
@@ -37,7 +57,11 @@ export declare function agreementAppUrl(origin: string, agreementId: string): st
|
|
|
37
57
|
export declare function agreementsListAppUrl(origin: string): string;
|
|
38
58
|
/** Where the human connects MCP servers and grants tools (ZIG-686). */
|
|
39
59
|
export declare function connectionsSettingsAppUrl(origin: string): string;
|
|
60
|
+
/** Where the human decides paused transfers (ZIG-896). */
|
|
61
|
+
export declare function walletAppUrl(origin: string): string;
|
|
40
62
|
export declare function buildPendingDecisionItems(inbox: InboxEnvelope, webOrigin: string): PendingDecisionItem[];
|
|
63
|
+
/** ZIG-896 — shape pending payment approvals for the session payload/card. */
|
|
64
|
+
export declare function buildPaymentApprovalItems(approvals: Array<Record<string, unknown>>, webOrigin: string): PaymentApprovalItem[];
|
|
41
65
|
/**
|
|
42
66
|
* ZIG-658: keep only tasks this delegate (or its principal) is expected to
|
|
43
67
|
* execute. The backend's GET /tasks returns every task reachable in the org,
|
|
@@ -63,6 +87,7 @@ export declare function buildSessionChatCard(decisions: PendingDecisionItem[], w
|
|
|
63
87
|
truncatedProposals?: number;
|
|
64
88
|
truncatedConnectionRequests?: number;
|
|
65
89
|
agreementsListAppUrl?: string;
|
|
90
|
+
paymentApprovals?: PaymentApprovalItem[];
|
|
66
91
|
}): string;
|
|
67
92
|
/** Structured session payload for MCP tools (ZIG-625 + active work). */
|
|
68
93
|
export declare function formatPendingDecisionsPayload(inbox: InboxEnvelope, webOrigin: string, opts?: {
|
|
@@ -75,5 +100,8 @@ export declare function formatPendingDecisionsPayload(inbox: InboxEnvelope, webO
|
|
|
75
100
|
* the card is not duplicated across every session-start tool.
|
|
76
101
|
*/
|
|
77
102
|
withSessionCard?: boolean;
|
|
103
|
+
/** ZIG-896 — pending payment approvals (GET /payments/approvals rows). */
|
|
104
|
+
paymentApprovals?: Array<Record<string, unknown>>;
|
|
105
|
+
paymentApprovalsError?: string;
|
|
78
106
|
}): Record<string, unknown>;
|
|
79
107
|
export declare function buildPendingNextActions(decisions: PendingDecisionItem[], work?: ActiveWorkItem[]): string[];
|
package/dist/pendingDecisions.js
CHANGED
|
@@ -20,6 +20,10 @@ export function agreementsListAppUrl(origin) {
|
|
|
20
20
|
export function connectionsSettingsAppUrl(origin) {
|
|
21
21
|
return `${origin}/app/settings/connections`;
|
|
22
22
|
}
|
|
23
|
+
/** Where the human decides paused transfers (ZIG-896). */
|
|
24
|
+
export function walletAppUrl(origin) {
|
|
25
|
+
return `${origin}/app/wallet`;
|
|
26
|
+
}
|
|
23
27
|
function truncateText(text, max = TITLE_MAX) {
|
|
24
28
|
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
25
29
|
if (oneLine.length <= max)
|
|
@@ -96,6 +100,27 @@ export function buildPendingDecisionItems(inbox, webOrigin) {
|
|
|
96
100
|
}
|
|
97
101
|
return items;
|
|
98
102
|
}
|
|
103
|
+
/** ZIG-896 — shape pending payment approvals for the session payload/card. */
|
|
104
|
+
export function buildPaymentApprovalItems(approvals, webOrigin) {
|
|
105
|
+
return approvals.map((a) => {
|
|
106
|
+
const payload = a['payload'] ?? {};
|
|
107
|
+
const approvalId = String(a['approvalId'] ?? '');
|
|
108
|
+
const requestedAt = a['createdAt'] ? String(a['createdAt']) : null;
|
|
109
|
+
return {
|
|
110
|
+
approvalId,
|
|
111
|
+
amount: typeof payload['amount'] === 'number' ? payload['amount'] : null,
|
|
112
|
+
toWalletId: payload['toWalletId'] || null,
|
|
113
|
+
description: payload['description'] || null,
|
|
114
|
+
reason: a['reason'] || null,
|
|
115
|
+
initiatorId: a['initiatorId'] || null,
|
|
116
|
+
requestedAt,
|
|
117
|
+
requestedAtLabel: formatWhen(requestedAt),
|
|
118
|
+
expiresAt: a['expiresAt'] ? String(a['expiresAt']) : null,
|
|
119
|
+
appUrl: walletAppUrl(webOrigin),
|
|
120
|
+
waitTool: `ziggs_payment_wait_for_approval approvalId=${approvalId}`,
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
99
124
|
/**
|
|
100
125
|
* ZIG-658: keep only tasks this delegate (or its principal) is expected to
|
|
101
126
|
* execute. The backend's GET /tasks returns every task reachable in the org,
|
|
@@ -194,6 +219,40 @@ function buildDecisionSection(items, opts) {
|
|
|
194
219
|
}
|
|
195
220
|
return lines;
|
|
196
221
|
}
|
|
222
|
+
/** ZIG-896 — paused transfers section: the human decides on the wallet page. */
|
|
223
|
+
function buildPaymentApprovalSection(items, startIndex = 1) {
|
|
224
|
+
if (!items.length)
|
|
225
|
+
return [];
|
|
226
|
+
const lines = [];
|
|
227
|
+
let n = startIndex;
|
|
228
|
+
for (const item of items) {
|
|
229
|
+
lines.push('---');
|
|
230
|
+
lines.push('');
|
|
231
|
+
lines.push(`#### ${n}. 💸 Payment approval`);
|
|
232
|
+
n += 1;
|
|
233
|
+
lines.push('');
|
|
234
|
+
const what = item.amount != null
|
|
235
|
+
? `**Transfer ${item.amount} cents${item.toWalletId ? ` → \`${item.toWalletId}\`` : ''}**`
|
|
236
|
+
: '**Paused transfer**';
|
|
237
|
+
lines.push(what);
|
|
238
|
+
lines.push('');
|
|
239
|
+
lines.push(`\`${item.approvalId}\`${item.requestedAtLabel ? ` · ${item.requestedAtLabel}` : ''}`);
|
|
240
|
+
lines.push('');
|
|
241
|
+
lines.push("_A transfer above your spending policy is paused — approve or reject it on the wallet page. The agent cannot decide this for you (there is no agent-side approve tool). It expires if you don't decide._");
|
|
242
|
+
if (item.reason || item.description) {
|
|
243
|
+
lines.push('');
|
|
244
|
+
lines.push(`> ${item.reason || item.description}`);
|
|
245
|
+
}
|
|
246
|
+
lines.push('');
|
|
247
|
+
lines.push(`[Decide in Ziggs wallet →](${item.appUrl})`);
|
|
248
|
+
lines.push('');
|
|
249
|
+
lines.push('| You say in chat | What the agent runs |');
|
|
250
|
+
lines.push('|:----------------|:--------------------|');
|
|
251
|
+
lines.push(`| \`wait for ${item.approvalId}\` | \`${item.waitTool}\` |`);
|
|
252
|
+
lines.push('');
|
|
253
|
+
}
|
|
254
|
+
return lines;
|
|
255
|
+
}
|
|
197
256
|
function buildWorkSection(work, startIndex = 1) {
|
|
198
257
|
if (!work.length)
|
|
199
258
|
return [];
|
|
@@ -237,7 +296,8 @@ export function buildSessionChatCard(decisions, work, opts) {
|
|
|
237
296
|
const truncatedProposals = opts.truncatedProposals ?? 0;
|
|
238
297
|
const truncatedConnectionRequests = opts.truncatedConnectionRequests ?? 0;
|
|
239
298
|
const truncated = truncatedProposals + truncatedConnectionRequests;
|
|
240
|
-
const
|
|
299
|
+
const paymentApprovals = opts.paymentApprovals ?? [];
|
|
300
|
+
const decisionListed = decisions.length + truncated + paymentApprovals.length;
|
|
241
301
|
const workCount = work.length;
|
|
242
302
|
const total = decisionListed + workCount;
|
|
243
303
|
if (total === 0)
|
|
@@ -252,6 +312,9 @@ export function buildSessionChatCard(decisions, work, opts) {
|
|
|
252
312
|
`| Approve / reject | **${decisionListed}** |`,
|
|
253
313
|
`| — proposals | ${decisions.filter((d) => d.kind === 'proposal').length}${truncatedProposals ? ` (+${truncatedProposals} hidden)` : ''} |`,
|
|
254
314
|
`| — link requests | ${decisions.filter((d) => d.kind === 'link_request').length}${truncatedConnectionRequests ? ` (+${truncatedConnectionRequests} hidden)` : ''} |`,
|
|
315
|
+
...(paymentApprovals.length > 0
|
|
316
|
+
? [`| — payment approvals | ${paymentApprovals.length} |`]
|
|
317
|
+
: []),
|
|
255
318
|
]
|
|
256
319
|
: []),
|
|
257
320
|
...(workCount > 0 ? [`| Active tasks (your work) | **${workCount}** |`] : []),
|
|
@@ -264,6 +327,10 @@ export function buildSessionChatCard(decisions, work, opts) {
|
|
|
264
327
|
lines.push(...buildDecisionSection(decisions, { ...opts, startIndex: sectionIndex }));
|
|
265
328
|
sectionIndex += decisions.length;
|
|
266
329
|
}
|
|
330
|
+
if (paymentApprovals.length) {
|
|
331
|
+
lines.push(...buildPaymentApprovalSection(paymentApprovals, sectionIndex));
|
|
332
|
+
sectionIndex += paymentApprovals.length;
|
|
333
|
+
}
|
|
267
334
|
if (work.length) {
|
|
268
335
|
lines.push(...buildWorkSection(work, sectionIndex));
|
|
269
336
|
}
|
|
@@ -278,9 +345,13 @@ export function formatPendingDecisionsPayload(inbox, webOrigin, opts) {
|
|
|
278
345
|
const withSessionCard = opts?.withSessionCard !== false;
|
|
279
346
|
const decisions = buildPendingDecisionItems(inbox, webOrigin);
|
|
280
347
|
const activeWork = buildActiveWorkItems(opts?.activeTasks ?? [], webOrigin);
|
|
348
|
+
const paymentApprovals = buildPaymentApprovalItems(opts?.paymentApprovals ?? [], webOrigin);
|
|
281
349
|
const truncatedProposals = inbox.truncatedProposals ?? 0;
|
|
282
350
|
const truncatedConnectionRequests = inbox.truncatedConnectionRequests ?? 0;
|
|
283
|
-
const pendingCount = decisions.length +
|
|
351
|
+
const pendingCount = decisions.length +
|
|
352
|
+
truncatedProposals +
|
|
353
|
+
truncatedConnectionRequests +
|
|
354
|
+
paymentApprovals.length;
|
|
284
355
|
const activeWorkCount = activeWork.length;
|
|
285
356
|
const actionCount = pendingCount + activeWorkCount;
|
|
286
357
|
const listUrl = agreementsListAppUrl(webOrigin);
|
|
@@ -288,6 +359,7 @@ export function formatPendingDecisionsPayload(inbox, webOrigin, opts) {
|
|
|
288
359
|
truncatedProposals,
|
|
289
360
|
truncatedConnectionRequests,
|
|
290
361
|
agreementsListAppUrl: listUrl,
|
|
362
|
+
paymentApprovals,
|
|
291
363
|
};
|
|
292
364
|
const sessionChatCard = buildSessionChatCard(decisions, activeWork, cardOpts);
|
|
293
365
|
const proposalCount = decisions.filter((d) => d.kind === 'proposal').length;
|
|
@@ -307,11 +379,13 @@ export function formatPendingDecisionsPayload(inbox, webOrigin, opts) {
|
|
|
307
379
|
summary: {
|
|
308
380
|
proposals: proposalCount + truncatedProposals,
|
|
309
381
|
linkRequests: linkCount + truncatedConnectionRequests,
|
|
382
|
+
paymentApprovals: paymentApprovals.length,
|
|
310
383
|
activeTasks: activeWorkCount,
|
|
311
|
-
listed: decisions.length,
|
|
384
|
+
listed: decisions.length + paymentApprovals.length,
|
|
312
385
|
truncated: truncatedProposals + truncatedConnectionRequests,
|
|
313
386
|
},
|
|
314
387
|
decisions,
|
|
388
|
+
paymentApprovals,
|
|
315
389
|
activeWork,
|
|
316
390
|
truncatedProposals,
|
|
317
391
|
truncatedConnectionRequests,
|
|
@@ -336,6 +410,13 @@ export function formatPendingDecisionsPayload(inbox, webOrigin, opts) {
|
|
|
336
410
|
workCardOmitted: true,
|
|
337
411
|
}
|
|
338
412
|
: {}),
|
|
413
|
+
// ZIG-896 — same signal for the approvals fetch: hasPending must not read
|
|
414
|
+
// as "no paused transfers" when the approvals call failed.
|
|
415
|
+
...(opts?.paymentApprovalsError
|
|
416
|
+
? {
|
|
417
|
+
paymentApprovalsFetchError: `Could not load payment approvals: ${opts.paymentApprovalsError}`,
|
|
418
|
+
}
|
|
419
|
+
: {}),
|
|
339
420
|
instruction,
|
|
340
421
|
};
|
|
341
422
|
}
|
package/dist/toolError.js
CHANGED
|
@@ -13,7 +13,7 @@ const HTTP_STATUS = /(?:^|\s)([1-5]\d{2})(?=\s|$)/;
|
|
|
13
13
|
const SCOPE_DENIED_HINT = 'You are not authorized for this scope. To get access: ask the counterparty ' +
|
|
14
14
|
'to issue you a context grant (they run ziggs_issue_grant), or request a ' +
|
|
15
15
|
'bilateral link first (ziggs_request_link). Check what you can already ' +
|
|
16
|
-
'reach with
|
|
16
|
+
'reach with ziggs_list_grants / ziggs_context_snapshot.';
|
|
17
17
|
function codeForStatus(status) {
|
|
18
18
|
if (status === 401)
|
|
19
19
|
return 'NOT_AUTHENTICATED';
|
package/dist/tools.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { getAgreement, getMyAgreements, listMyChats, openConversation, proposeDirectTo, proposeBroadcast, publishOffer, claimOffer, provisionRelayWorkers, respondToAgreement, revokeAgreement, sendChatMessage, ContextDiscoveryClient, ContextReadClient, GrantsClient, InboxClient, createTask, updateTaskState, replaceTaskPlan, listTasks, getBackendUrl, } from '@ziggs-ai/api-client';
|
|
3
|
+
import { getAgreement, getMyAgreements, listMyChats, openConversation, proposeDirectTo, proposeBroadcast, publishOffer, claimOffer, provisionRelayWorkers, respondToAgreement, revokeAgreement, counterAgreement, fulfillAgreement, sendChatMessage, ConnectionsClient, PaymentsClient, assertNoLeakedConnectionSecret, ContextDiscoveryClient, ContextReadClient, ContextGrantsClient, GrantsClient, unreadableGrantRails, InboxClient, createTask, updateTaskState, replaceTaskPlan, listTasks, getBackendUrl, ArtifactsClient, } from '@ziggs-ai/api-client';
|
|
4
4
|
import { decodeOperatorKeyClaims } from './operatorKey.js';
|
|
5
5
|
import { registerTrustTools } from './trustTools.js';
|
|
6
|
+
import { registerPaymentTools } from './paymentTools.js';
|
|
6
7
|
import { formatInboxToolResult, buildReadContextReadPlan, } from './inboxToolResult.js';
|
|
7
8
|
import { filterTasksForDelegate, formatPendingDecisionsPayload, resolveWebAppOrigin, buildPendingNextActions, } from './pendingDecisions.js';
|
|
8
9
|
import { PROTOCOL } from './protocol/delegateProtocol.js';
|
|
@@ -26,7 +27,7 @@ const ZIGGS_INBOX_DESCRIPTION = "What's new since your last ack — references o
|
|
|
26
27
|
`${PROTOCOL.humanAttention} ${PROTOCOL.pendingDecisions} ` +
|
|
27
28
|
'When hasActionable the response carries the pending/active counts and points to ziggs_pending_decisions for the sessionChatCard to paste (that tool owns the card; it is not duplicated here). ' +
|
|
28
29
|
'A `readPlan` array gives the exact next calls (tool + pre-filled args) for the news in this response — run them verbatim to read each scope and ack; when the plan overflows, `readPlanTruncated` counts the reads it dropped (the ack call is always kept). ' +
|
|
29
|
-
'Each scope also carries the covering `grant` (grantId, temporal, watermarkAt, expiresAt), and readPlan reads come pre-pinned with that contextGrantId, so no separate
|
|
30
|
+
'Each scope also carries the covering `grant` (grantId, temporal, watermarkAt, expiresAt), and readPlan reads come pre-pinned with that contextGrantId, so no separate ziggs_list_grants call is needed. ' +
|
|
30
31
|
`${PROTOCOL.loop} ${PROTOCOL.ack}`;
|
|
31
32
|
const ZIGGS_PENDING_DECISIONS_DESCRIPTION = 'Session start summary: approve/reject decisions AND active tasks assigned to your delegate. ' +
|
|
32
33
|
'Call at session start in Cursor/Claude — pull-only MCP has no notification tray. ' +
|
|
@@ -66,86 +67,10 @@ const contextReadTypeSchema = z.enum([
|
|
|
66
67
|
'tasks',
|
|
67
68
|
]);
|
|
68
69
|
const artifactVisibilitySchema = z.enum(['chat', 'agent-private']);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
headers: {
|
|
74
|
-
'content-type': 'application/json',
|
|
75
|
-
Authorization: `Bearer ${creds.operatorKey}`,
|
|
76
|
-
'X-Agent-Id': creds.agentId,
|
|
77
|
-
},
|
|
78
|
-
body: JSON.stringify({
|
|
79
|
-
text: input.text.trim(),
|
|
80
|
-
content_type: input.content_type ?? 'text',
|
|
81
|
-
visibility: input.visibility,
|
|
82
|
-
chatId: input.chatId,
|
|
83
|
-
agreementId: input.agreementId,
|
|
84
|
-
taskId: input.taskId,
|
|
85
|
-
}),
|
|
86
|
-
});
|
|
87
|
-
if (!res.ok) {
|
|
88
|
-
const body = await res.text().catch(() => '');
|
|
89
|
-
throw new Error(`POST /artifacts ${res.status} ${body.slice(0, 200)}`);
|
|
90
|
-
}
|
|
91
|
-
const body = (await res.json().catch(() => ({})));
|
|
92
|
-
return { artifactId: body.artifactId };
|
|
93
|
-
}
|
|
94
|
-
// ZIG-569 — defense-in-depth mirror of the backend leak-guard
|
|
95
|
-
// (assertProxyResponseDoesNotLeakTokens). The backend strips the *specific*
|
|
96
|
-
// vault token from the response; the MCP layer never sees that token, so it
|
|
97
|
-
// instead pattern-scans the proxied body for high-signal provider credential
|
|
98
|
-
// shapes and refuses to hand a likely-leaked secret to the model.
|
|
99
|
-
const LEAKED_SECRET_PATTERNS = [
|
|
100
|
-
/\bgh[posru]_[A-Za-z0-9]{16,}\b/, // GitHub PAT / OAuth / user / server / refresh
|
|
101
|
-
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/, // fine-grained GitHub PAT
|
|
102
|
-
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, // Slack bot/user/app/refresh tokens
|
|
103
|
-
/\bxapp-[A-Za-z0-9-]{10,}\b/, // Slack app-level token
|
|
104
|
-
/"(?:access_token|refresh_token)"\s*:\s*"[^"]{8,}"/, // raw OAuth token JSON keys
|
|
105
|
-
];
|
|
106
|
-
function assertNoLeakedSecret(serialized) {
|
|
107
|
-
for (const re of LEAKED_SECRET_PATTERNS) {
|
|
108
|
-
if (re.test(serialized)) {
|
|
109
|
-
throw new Error('connection proxy response withheld: it appears to contain a credential (token-leak guard)');
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* ZIG-569 — call the backend connections proxy so the agent can use a stored
|
|
115
|
-
* connection without ever seeing the credential. Impersonates the grant-holder
|
|
116
|
-
* agent (X-Agent-Id, required by the broker), returns the provider `result`, and
|
|
117
|
-
* mirrors the backend leak-guard before the body reaches the model.
|
|
118
|
-
*/
|
|
119
|
-
async function proxyConnection(creds, input) {
|
|
120
|
-
const url = `${getBackendUrl()}/connections/${encodeURIComponent(input.connectionId)}/proxy`;
|
|
121
|
-
const res = await fetch(url, {
|
|
122
|
-
method: 'POST',
|
|
123
|
-
headers: {
|
|
124
|
-
'content-type': 'application/json',
|
|
125
|
-
Authorization: `Bearer ${creds.operatorKey}`,
|
|
126
|
-
'X-Agent-Id': creds.agentId,
|
|
127
|
-
},
|
|
128
|
-
body: JSON.stringify({
|
|
129
|
-
grantId: input.grantId,
|
|
130
|
-
action: input.action,
|
|
131
|
-
payload: input.payload ?? {},
|
|
132
|
-
}),
|
|
133
|
-
});
|
|
134
|
-
const body = await res.text().catch(() => '');
|
|
135
|
-
if (!res.ok) {
|
|
136
|
-
throw new Error(`POST /connections/${input.connectionId}/proxy ${res.status} ${body.slice(0, 200)}`);
|
|
137
|
-
}
|
|
138
|
-
assertNoLeakedSecret(body);
|
|
139
|
-
let parsed = body;
|
|
140
|
-
try {
|
|
141
|
-
parsed = body ? JSON.parse(body) : null;
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
parsed = body;
|
|
145
|
-
}
|
|
146
|
-
const result = parsed?.['result'];
|
|
147
|
-
return result ?? parsed;
|
|
148
|
-
}
|
|
70
|
+
// ZIG-899 — the strict artifact write (fail loudly, return the artifactId)
|
|
71
|
+
// moved into ArtifactsClient.writeStrict, shared with the SDK's record_artifact.
|
|
72
|
+
// ZIG-894 — the leak-guard and the connections proxy/request calls moved into
|
|
73
|
+
// @ziggs-ai/api-client's ConnectionsClient (shared with the agent SDK).
|
|
149
74
|
/** ZIG-640 — runtime acting org from server (self-hire / agent row). */
|
|
150
75
|
async function fetchDelegateAccess(creds) {
|
|
151
76
|
const url = `${getBackendUrl()}/agents/claude-delegate/access`;
|
|
@@ -164,7 +89,7 @@ async function fetchDelegateAccess(creds) {
|
|
|
164
89
|
}
|
|
165
90
|
/**
|
|
166
91
|
* ZIG-739 — the operator's full org membership (not just granted scopes, which
|
|
167
|
-
* is all
|
|
92
|
+
* is all ziggs_list_grants sees). Lets the delegate resolve an org name to
|
|
168
93
|
* an id and offer a pick-list instead of demanding a pasted org_... id.
|
|
169
94
|
*/
|
|
170
95
|
async function fetchMyOrgs(creds) {
|
|
@@ -230,38 +155,9 @@ async function listConnectionsForHolder(creds) {
|
|
|
230
155
|
group.grants.push(g);
|
|
231
156
|
}
|
|
232
157
|
const result = [...byConnection.values()];
|
|
233
|
-
|
|
158
|
+
assertNoLeakedConnectionSecret(JSON.stringify(result));
|
|
234
159
|
return result;
|
|
235
160
|
}
|
|
236
|
-
/**
|
|
237
|
-
* ZIG-686 — agent-initiated MCP connection request: ask the principal to
|
|
238
|
-
* connect a remote MCP server and grant this agent the listed tools. Opens a
|
|
239
|
-
* connection-consent agreement in the working chat (ZIG-798): the human
|
|
240
|
-
* approves it there like any agreement, and on approval the server is connected
|
|
241
|
-
* (if needed) and this agent is granted the tools. Returns the agreement id.
|
|
242
|
-
*/
|
|
243
|
-
async function createMcpConnectionRequest(creds, input) {
|
|
244
|
-
const url = `${getBackendUrl()}/connections/mcp/requests`;
|
|
245
|
-
const res = await fetch(url, {
|
|
246
|
-
method: 'POST',
|
|
247
|
-
headers: {
|
|
248
|
-
'content-type': 'application/json',
|
|
249
|
-
Authorization: `Bearer ${creds.operatorKey}`,
|
|
250
|
-
'X-Agent-Id': creds.agentId,
|
|
251
|
-
},
|
|
252
|
-
body: JSON.stringify({
|
|
253
|
-
serverUrl: input.serverUrl,
|
|
254
|
-
tools: input.tools,
|
|
255
|
-
reason: input.reason,
|
|
256
|
-
chatId: input.chatId,
|
|
257
|
-
}),
|
|
258
|
-
});
|
|
259
|
-
const body = await res.text().catch(() => '');
|
|
260
|
-
if (!res.ok) {
|
|
261
|
-
throw new Error(`POST /connections/mcp/requests ${res.status} ${body.slice(0, 200)}`);
|
|
262
|
-
}
|
|
263
|
-
return body ? JSON.parse(body) : {};
|
|
264
|
-
}
|
|
265
161
|
/**
|
|
266
162
|
* Ids this delegate answers for: its own agent id plus its principal's user
|
|
267
163
|
* id (operator-key ownerId / ZIGGS_OWNER_USER_ID). Used to decide which
|
|
@@ -291,9 +187,21 @@ async function loadSessionActionsPayload(creds, cfg, opts) {
|
|
|
291
187
|
// failure so hasActiveWork:false is not mistaken for "no tasks".
|
|
292
188
|
activeTasksError = e.message;
|
|
293
189
|
}
|
|
190
|
+
// ZIG-896 — paused transfers awaiting the wallet owner. Same failure rule as
|
|
191
|
+
// tasks: a failed fetch is surfaced, not silently rendered as "none pending".
|
|
192
|
+
let paymentApprovals = [];
|
|
193
|
+
let paymentApprovalsError;
|
|
194
|
+
try {
|
|
195
|
+
paymentApprovals = (await new PaymentsClient(creds.operatorKey, creds.agentId).approvals({ status: 'pending' }));
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
paymentApprovalsError = e.message;
|
|
199
|
+
}
|
|
294
200
|
return formatPendingDecisionsPayload(inbox, webOrigin, {
|
|
295
201
|
activeTasks,
|
|
296
202
|
activeTasksError,
|
|
203
|
+
paymentApprovals,
|
|
204
|
+
paymentApprovalsError,
|
|
297
205
|
withSessionCard: opts?.withSessionCard,
|
|
298
206
|
});
|
|
299
207
|
}
|
|
@@ -369,7 +277,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
369
277
|
: 'Next: ziggs_inbox or ziggs_pending_decisions at session start.',
|
|
370
278
|
});
|
|
371
279
|
});
|
|
372
|
-
server.tool('ziggs_list_my_orgs', 'List every org you (the operator) belong to — { orgId, name, kind, role }. Unlike
|
|
280
|
+
server.tool('ziggs_list_my_orgs', 'List every org you (the operator) belong to — { orgId, name, kind, role }. Unlike ziggs_list_grants (granted scopes only), this is your full membership — useful before OAuth reconnect when the human wants to pick a target org.', {}, READ_ONLY, async () => {
|
|
373
281
|
try {
|
|
374
282
|
const orgs = await fetchMyOrgs(creds);
|
|
375
283
|
return textResult({ count: orgs.length, orgs });
|
|
@@ -378,25 +286,6 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
378
286
|
return toolError(e.message);
|
|
379
287
|
}
|
|
380
288
|
});
|
|
381
|
-
server.tool('ziggs_switch_org', 'Deprecated: org is fixed at OAuth consent. Reconnect MCP OAuth and pick the target org on the consent screen — this tool no longer switches org server-side.', {
|
|
382
|
-
org: z
|
|
383
|
-
.string()
|
|
384
|
-
.optional()
|
|
385
|
-
.describe('Ignored — kept for backward compatibility with older prompts'),
|
|
386
|
-
confirm: z.literal(true).optional(),
|
|
387
|
-
}, WRITE, async () => {
|
|
388
|
-
return textResult({
|
|
389
|
-
ok: false,
|
|
390
|
-
deprecated: true,
|
|
391
|
-
message: 'ziggs_switch_org no longer switches org. Org is bound at MCP OAuth consent. Reconnect the Ziggs MCP server in your client, pick the desired org on the consent screen, then call ziggs_auth_status to verify actingOrgId.',
|
|
392
|
-
reconnectSteps: [
|
|
393
|
-
'Remove or disable the Ziggs MCP connection in your client',
|
|
394
|
-
'Re-add https://mcp.ziggsai.com/mcp (or your .mcp.json entry)',
|
|
395
|
-
'On the Ziggs consent screen, select the org you want',
|
|
396
|
-
'Call ziggs_auth_status and confirm actingOrgId',
|
|
397
|
-
],
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
289
|
server.tool('ziggs_pending_decisions', ZIGGS_PENDING_DECISIONS_DESCRIPTION, {}, READ_ONLY, async () => {
|
|
401
290
|
try {
|
|
402
291
|
const payload = await loadSessionActionsPayload(creds, cfg);
|
|
@@ -708,6 +597,49 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
708
597
|
return toolError(e.message);
|
|
709
598
|
}
|
|
710
599
|
});
|
|
600
|
+
server.tool('ziggs_counter_agreement', 'Counter a pending proposal with revised terms instead of approving or rejecting (POST /agreements/:id/counter). Provide only the terms you want to change — price, description, expiry, lifecycle, or plan; omitted fields keep the original proposal\'s value. The counter goes back to the counterparty as a fresh pending proposal for them to approve/reject/counter. Read the current terms first with ziggs_get_agreement.', {
|
|
601
|
+
agreementId: z.string().describe('The pending agreement to counter'),
|
|
602
|
+
price: z.number().optional().describe('Revised price'),
|
|
603
|
+
agreementDescription: z
|
|
604
|
+
.string()
|
|
605
|
+
.optional()
|
|
606
|
+
.describe('Revised agreement description / terms'),
|
|
607
|
+
expiresAt: z.string().optional().describe('Revised expiry (ISO-8601)'),
|
|
608
|
+
lifecycle: z.string().optional().describe('Revised lifecycle'),
|
|
609
|
+
maxExecutions: z.number().optional().describe('Revised max executions'),
|
|
610
|
+
description: z
|
|
611
|
+
.string()
|
|
612
|
+
.optional()
|
|
613
|
+
.describe('Revised task description for the spawned work'),
|
|
614
|
+
plan: z
|
|
615
|
+
.record(z.unknown())
|
|
616
|
+
.optional()
|
|
617
|
+
.describe('Override plan { steps: [...] }; omit to keep the original'),
|
|
618
|
+
planReviewTiming: z
|
|
619
|
+
.enum(['with_proposal', 'before_execution'])
|
|
620
|
+
.optional()
|
|
621
|
+
.describe('When the buyer reviews the plan: with_proposal | before_execution'),
|
|
622
|
+
requireMidWorkPlanAck: z.boolean().optional(),
|
|
623
|
+
}, WRITE, async ({ agreementId, ...counter }) => {
|
|
624
|
+
try {
|
|
625
|
+
const agreement = await counterAgreement(agreementId, counter, creds);
|
|
626
|
+
return textResult({ status: 'countered', agreementId, agreement });
|
|
627
|
+
}
|
|
628
|
+
catch (e) {
|
|
629
|
+
return toolError(e.message);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
server.tool('ziggs_fulfill_agreement', 'Mark an agreement you PROVIDE as fulfilled/complete once its work is delivered (POST /agreements/:id/fulfill) — closes the engagement so it no longer reads as in-progress. Party-gated server-side: only the providing side can fulfill. Use on your hire after the final deliverable is done and delivered.', {
|
|
633
|
+
agreementId: z.string().describe('The agreement you provide, to mark fulfilled'),
|
|
634
|
+
}, WRITE, async ({ agreementId }) => {
|
|
635
|
+
try {
|
|
636
|
+
const result = await fulfillAgreement(agreementId, creds);
|
|
637
|
+
return textResult({ status: 'fulfilled', agreementId, agreement: result.agreement });
|
|
638
|
+
}
|
|
639
|
+
catch (e) {
|
|
640
|
+
return toolError(e.message);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
711
643
|
server.tool('ziggs_inbox', ZIGGS_INBOX_DESCRIPTION, {
|
|
712
644
|
ack: z
|
|
713
645
|
.array(z.object({
|
|
@@ -752,29 +684,61 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
752
684
|
return toolError(e.message);
|
|
753
685
|
}
|
|
754
686
|
});
|
|
755
|
-
server.tool('
|
|
687
|
+
server.tool('ziggs_list_grants', 'List every grant this delegate holds across all rails in one call — context (chat/agreement/org), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). The single answer to "what grants of mine do you hold?", holder-scoped and cross-session. Filter by scopeKind (rail) and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails, not silently dropped. Cursor-paginated: pass cursor from a prior nextCursor to page. Pass a grantId to ziggs_read_context to pin a specific grant, or ziggs_expand_context to enumerate a scope.', {
|
|
688
|
+
scopeKind: z
|
|
689
|
+
.array(z.enum(['chat', 'agreement', 'org', 'connection', 'wallet']))
|
|
690
|
+
.optional()
|
|
691
|
+
.describe('Rail filter (repeatable). Omit for every rail you can read.'),
|
|
692
|
+
health: z
|
|
693
|
+
.enum(['active', 'expired', 'revoked'])
|
|
694
|
+
.optional()
|
|
695
|
+
.describe('Grant health filter. Defaults to active (live grants only).'),
|
|
756
696
|
cursor: z
|
|
757
697
|
.string()
|
|
758
698
|
.optional()
|
|
759
699
|
.describe('Opaque cursor from a prior nextCursor'),
|
|
760
700
|
limit: z.number().optional().describe('Page size (default server-side)'),
|
|
761
|
-
}, READ_ONLY, async ({ cursor, limit }) => {
|
|
701
|
+
}, READ_ONLY, async ({ scopeKind, health, cursor, limit }) => {
|
|
762
702
|
try {
|
|
763
703
|
const client = new GrantsClient(creds.operatorKey, creds.agentId);
|
|
704
|
+
const kinds = scopeKind && scopeKind.length
|
|
705
|
+
? scopeKind
|
|
706
|
+
: undefined;
|
|
764
707
|
const { items, nextCursor } = await client.listGrants({
|
|
765
|
-
scopeKind:
|
|
766
|
-
// Reach = live grants only;
|
|
767
|
-
health: 'active',
|
|
708
|
+
scopeKind: kinds,
|
|
709
|
+
// Reach = live grants only by default; pass health for expired/revoked.
|
|
710
|
+
health: health ?? 'active',
|
|
768
711
|
cursor,
|
|
769
712
|
limit,
|
|
770
713
|
});
|
|
771
|
-
|
|
714
|
+
const unreadable = unreadableGrantRails(creds.operatorKey, kinds);
|
|
715
|
+
return textResult({
|
|
716
|
+
count: items.length,
|
|
717
|
+
grants: items,
|
|
718
|
+
nextCursor,
|
|
719
|
+
...(unreadable && unreadable.length
|
|
720
|
+
? { unreadableRails: unreadable }
|
|
721
|
+
: {}),
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
catch (e) {
|
|
725
|
+
return toolError(e.message);
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
server.tool('ziggs_expand_context', 'Expand a grant you hold into the chat/agreement ids inside its scope, so you can actually read through it. ziggs_list_grants tells you that you hold e.g. org:acme or agreement:x; this returns the { chats, agreements } (ids + labels only, never content) that scope covers — feed an id to ziggs_read_context (via=chat:<id> / agreement:<id>). Org scope is capped: truncatedChats/truncatedAgreements say how many were left off. Holder-only, grant-fenced.', {
|
|
729
|
+
grantId: z
|
|
730
|
+
.string()
|
|
731
|
+
.describe('A grant you hold (grantId from ziggs_list_grants) to expand'),
|
|
732
|
+
}, READ_ONLY, async ({ grantId }) => {
|
|
733
|
+
try {
|
|
734
|
+
const client = new ContextGrantsClient(creds.operatorKey, creds.agentId);
|
|
735
|
+
return textResult(await client.getReach(grantId));
|
|
772
736
|
}
|
|
773
737
|
catch (e) {
|
|
774
738
|
return toolError(e.message);
|
|
775
739
|
}
|
|
776
740
|
});
|
|
777
|
-
server.tool('ziggs_discover_grantable', 'See what context EXISTS in your orgs that you CANNOT read yet — so you can ask for it instead of failing blind. Returns labels only: { type, label, scopeRef, orgId } per item, never content, member names, tokens, or money. Bounded to orgs you have an active agreement in. To act on one, ask your human to grant it, or (if you hold a broader grant of your own) delegate via ziggs_delegate_grant using the scopeRef. Use
|
|
741
|
+
server.tool('ziggs_discover_grantable', 'See what context EXISTS in your orgs that you CANNOT read yet — so you can ask for it instead of failing blind. Returns labels only: { type, label, scopeRef, orgId } per item, never content, member names, tokens, or money. Bounded to orgs you have an active agreement in. To act on one, ask your human to grant it, or (if you hold a broader grant of your own) delegate via ziggs_delegate_grant using the scopeRef. Use ziggs_list_grants for what you already hold; this is what you lack.', {}, READ_ONLY, async () => {
|
|
778
742
|
try {
|
|
779
743
|
const client = new ContextDiscoveryClient(creds.operatorKey, creds.agentId);
|
|
780
744
|
const items = await client.discoverGrantable();
|
|
@@ -841,14 +805,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
841
805
|
if ((chatId && agreementId) || (!chatId && !agreementId)) {
|
|
842
806
|
return toolError('Pass exactly one of chatId or agreementId');
|
|
843
807
|
}
|
|
844
|
-
const { artifactId } = await
|
|
845
|
-
text,
|
|
846
|
-
visibility,
|
|
847
|
-
chatId,
|
|
848
|
-
agreementId,
|
|
849
|
-
taskId,
|
|
850
|
-
content_type,
|
|
851
|
-
});
|
|
808
|
+
const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId).writeStrict({ text, visibility, chatId, agreementId, taskId, content_type });
|
|
852
809
|
return textResult({
|
|
853
810
|
ok: true,
|
|
854
811
|
artifactId,
|
|
@@ -970,12 +927,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
970
927
|
.describe('Action-specific arguments (provider-defined)'),
|
|
971
928
|
}, WRITE, async ({ connectionId, grantId, action, payload }) => {
|
|
972
929
|
try {
|
|
973
|
-
const result = await
|
|
974
|
-
connectionId,
|
|
975
|
-
grantId,
|
|
976
|
-
action,
|
|
977
|
-
payload,
|
|
978
|
-
});
|
|
930
|
+
const result = await new ConnectionsClient(creds.operatorKey, creds.agentId).proxy({ connectionId, grantId, action, payload });
|
|
979
931
|
return textResult({ ok: true, action, result });
|
|
980
932
|
}
|
|
981
933
|
catch (e) {
|
|
@@ -1009,12 +961,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
1009
961
|
.describe('Plain-language reason shown to the human deciding'),
|
|
1010
962
|
}, WRITE, async ({ chatId, serverUrl, tools, reason }) => {
|
|
1011
963
|
try {
|
|
1012
|
-
const result = await
|
|
1013
|
-
chatId,
|
|
1014
|
-
serverUrl,
|
|
1015
|
-
tools,
|
|
1016
|
-
reason,
|
|
1017
|
-
});
|
|
964
|
+
const result = await new ConnectionsClient(creds.operatorKey, creds.agentId).requestMcpConnection({ chatId, serverUrl, tools, reason });
|
|
1018
965
|
return textResult({
|
|
1019
966
|
ok: true,
|
|
1020
967
|
...result,
|
|
@@ -1027,4 +974,5 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
1027
974
|
}
|
|
1028
975
|
});
|
|
1029
976
|
registerTrustTools(server, creds, cfg);
|
|
977
|
+
registerPaymentTools(server, creds);
|
|
1030
978
|
}
|
package/dist/trustTools.js
CHANGED
|
@@ -57,6 +57,22 @@ export function registerTrustTools(server, creds, cfg) {
|
|
|
57
57
|
return toolError(e.message);
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
|
+
server.tool('ziggs_get_agent', 'Fetch the full profile of ONE agent by its exact id (GET /agents/:id) — name, description, tags, capabilities, reachability, and reliability. Use to confirm a candidate before ziggs_propose_agreement / ziggs_request_link, when you already hold the agent id (from ziggs_search_agents, a grant, or an agreement party). Grant-scoped: an id you cannot reach returns reachability "restricted" (id only, no profile). To find an agent by keyword instead, use ziggs_search_agents.', {
|
|
61
|
+
agentId: z.string().describe('Exact agent id to fetch — do not guess'),
|
|
62
|
+
}, READ_ONLY, async ({ agentId }) => {
|
|
63
|
+
try {
|
|
64
|
+
const client = new AgentSearchClient(creds.operatorKey, creds.agentId);
|
|
65
|
+
const result = await client.getAgentById(agentId);
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
return toolError(result.error ?? 'agent not found');
|
|
68
|
+
}
|
|
69
|
+
const { success: _success, ...agent } = result;
|
|
70
|
+
return textResult({ agent });
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
return toolError(e.message);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
60
76
|
server.tool('ziggs_issue_grant', 'Issue bounded context access. Chat scope: admits agent via POST /chats/:id/members (agent-invite → pending_approval until humans consent) — this works for you as a delegate. Agreement/org scope: issuing a NEW root grant is a human-authority action; if you are acting for a principal you are denied (AGENT_LACKS_HUMAN_AUTHORITY) — instead use ziggs_delegate_grant to hand a peer a narrower slice of a grant you already hold, or ask your human to issue it. Defaults: from-now, narrow scope.', {
|
|
61
77
|
holderId: z.string().describe('Bare agent id receiving the grant'),
|
|
62
78
|
scopeKind: grantScopeKindSchema,
|
package/examples/claude-code.md
CHANGED
|
@@ -51,7 +51,7 @@ Optional env:
|
|
|
51
51
|
|
|
52
52
|
Ask Claude to call tools in order:
|
|
53
53
|
|
|
54
|
-
1. `ziggs_list_chats` or `
|
|
54
|
+
1. `ziggs_list_chats` or `ziggs_list_grants`
|
|
55
55
|
2. `ziggs_send_message` (chat you belong to)
|
|
56
56
|
3. `ziggs_propose_agreement` + `ziggs_respond_to_agreement` (optional)
|
|
57
57
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/ziggs-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
4
4
|
"description": "MCP server for Claude Code, Cursor, and other MCP hosts — act as your Ziggs delegate agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,10 +36,13 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
39
|
-
"@ziggs-ai/api-client": "^0.1.
|
|
39
|
+
"@ziggs-ai/api-client": "^0.1.30",
|
|
40
40
|
"dotenv": "^16.6.1",
|
|
41
41
|
"zod": "^3.24.2"
|
|
42
42
|
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@ziggs-ai/agent-sdk": "^0.2.2"
|
|
45
|
+
},
|
|
43
46
|
"engines": {
|
|
44
47
|
"node": ">=20"
|
|
45
48
|
},
|
package/skills/ziggs/SKILL.md
CHANGED
|
@@ -45,7 +45,7 @@ The sections below elaborate this protocol with tools, examples, and edge cases.
|
|
|
45
45
|
4. Read the envelope: scope news counts, `humanAttention`, and **`decisionChatCard`** when present.
|
|
46
46
|
5. Do **not** pull full scope history “just in case.” Only read scopes that show news or that you must act on.
|
|
47
47
|
|
|
48
|
-
If `ziggs_inbox` is unavailable, fall back to **`
|
|
48
|
+
If `ziggs_inbox` is unavailable, fall back to **`ziggs_list_grants`** (scopeKind: chat/agreement/org) to list reachable scopes, then **`ziggs_read_context`** with **`after`** cursors — still inbox-first in spirit (delta reads only).
|
|
49
49
|
|
|
50
50
|
## The working loop
|
|
51
51
|
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Reach is grant-gated
|
|
4
4
|
|
|
5
|
-
You only read context your delegate **holds a grant for**. `
|
|
5
|
+
You only read context your delegate **holds a grant for**. `ziggs_list_grants` lists every grant you hold across all rails (context / connection / wallet); `ziggs_read_context` enforces grants on every read.
|
|
6
|
+
|
|
7
|
+
To answer "what grants of mine do you hold?", call **`ziggs_list_grants`** — one call, all rails, cross-session. Filter by `scopeKind` for a single rail; rails you can't read are named in `unreadableRails`.
|
|
6
8
|
|
|
7
9
|
## Before issuing grants
|
|
8
10
|
|
|
@@ -14,7 +16,7 @@ Ask the human unless they already specified in this session:
|
|
|
14
16
|
| `from-now` or `from-start`? | `from-start` exposes history — often needs counterparty approval |
|
|
15
17
|
| Expiry / purpose? | Revocation and audit trail |
|
|
16
18
|
|
|
17
|
-
Use **`
|
|
19
|
+
Use **`ziggs_list_grants`** (scopeKind: chat/agreement/org) to see existing reach before adding more.
|
|
18
20
|
|
|
19
21
|
## Approval gates
|
|
20
22
|
|
|
@@ -24,9 +26,14 @@ These commonly surface as **`pending_approval`** or blocked tool errors:
|
|
|
24
26
|
- `from-start` history on a scope
|
|
25
27
|
- Cross-org grant issue / delegate
|
|
26
28
|
- Agreement steps that require a human principal
|
|
29
|
+
- A `ziggs_payment_transfer` above the wallet owner's spending policy (`approval_required` — decided on the wallet page, never by the agent)
|
|
27
30
|
|
|
28
31
|
**Default:** present the pending item to the human with id, title, and recommended action — do not approve silently.
|
|
29
32
|
|
|
33
|
+
## Payments (wallet rail)
|
|
34
|
+
|
|
35
|
+
Spending rides a **payment grant** the wallet owner issued (`ziggs_payment_issue_grant`; find held grants via `ziggs_list_grants` scopeKind=wallet, pass the grantId as `paymentGrantId` on `ziggs_payment_transfer`). Escrow: `ziggs_payment_hold` → `ziggs_payment_release`. A transfer above policy pauses as `approval_required`: it shows in `ziggs_pending_decisions`, the human decides on the wallet page, and you may poll briefly with `ziggs_payment_wait_for_approval`. There is deliberately **no agent-side approve tool** — never try to decide your own transfer.
|
|
36
|
+
|
|
30
37
|
## Trust tool sequence (cross-org)
|
|
31
38
|
|
|
32
39
|
A **link** (bilateral agent-to-agent trust — not a `ziggs_connection_proxy` third-party
|