@ziggs-ai/ziggs-mcp 0.1.33 → 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/inboxToolResult.d.ts +3 -3
- package/dist/inboxToolResult.js +12 -10
- 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 +158 -228
- package/dist/trustTools.js +20 -4
- package/examples/claude-code.md +1 -1
- package/package.json +5 -2
- package/skills/ziggs/SKILL.md +3 -3
- 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
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { GrantView, ContextReadType, InboxAckResult, InboxEnvelope, Task } from '@ziggs-ai/api-client';
|
|
2
2
|
/**
|
|
3
3
|
* ZIG-634 (Step 1): a pre-filled next call. The agent can run it verbatim
|
|
4
4
|
* instead of assembling args from the ids scattered through the response.
|
|
@@ -64,7 +64,7 @@ export interface ScopeGrantTag {
|
|
|
64
64
|
* list is already the caller's non-expired grants (holderId == principalId),
|
|
65
65
|
* so this is grant metadata the caller already holds — no protected content.
|
|
66
66
|
*/
|
|
67
|
-
export declare function indexReachByScope(reach:
|
|
67
|
+
export declare function indexReachByScope(reach: GrantView[]): Map<string, ScopeGrantTag>;
|
|
68
68
|
/**
|
|
69
69
|
* Put humanAttention first so MCP hosts surface it before counts (ZIG-482),
|
|
70
70
|
* and append readPlan last so each inbox call self-narrates the follow-up
|
|
@@ -73,4 +73,4 @@ export declare function indexReachByScope(reach: ContextReachDescriptor[]): Map<
|
|
|
73
73
|
* When `reach` (the caller's own live grants) is passed, each scope is tagged
|
|
74
74
|
* with its covering grant (ZIG-635) so the agent can pin X-Context-Grant-Id.
|
|
75
75
|
*/
|
|
76
|
-
export declare function formatInboxToolResult(inbox: InboxEnvelope, ack?: InboxAckResult | null, webOrigin?: string, activeTasks?: Task[], reach?:
|
|
76
|
+
export declare function formatInboxToolResult(inbox: InboxEnvelope, ack?: InboxAckResult | null, webOrigin?: string, activeTasks?: Task[], reach?: GrantView[], activeTasksError?: string): Record<string, unknown>;
|
package/dist/inboxToolResult.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { grantCaveat } from '@ziggs-ai/api-client';
|
|
1
2
|
import { formatPendingDecisionsPayload, resolveWebAppOrigin, } from './pendingDecisions.js';
|
|
2
3
|
/** Keep the plan bounded; the full scopes array still carries everything. */
|
|
3
4
|
const MAX_READ_PLAN = 12;
|
|
@@ -147,13 +148,14 @@ export function buildReadContextReadPlan(page, type, via, presentedGrantId) {
|
|
|
147
148
|
}
|
|
148
149
|
return plan;
|
|
149
150
|
}
|
|
150
|
-
function toScopeGrantTag(
|
|
151
|
+
function toScopeGrantTag(g) {
|
|
151
152
|
return {
|
|
152
|
-
grantId:
|
|
153
|
-
temporal
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
grantId: g.grantId,
|
|
154
|
+
// Context grants carry temporal/watermark as caveats (ZIG-646).
|
|
155
|
+
temporal: grantCaveat(g, 'temporal') ?? 'from-now',
|
|
156
|
+
watermarkAt: grantCaveat(g, 'watermark_at') ?? '',
|
|
157
|
+
expiresAt: g.expiresAt,
|
|
158
|
+
parentGrantId: g.parentGrantId,
|
|
157
159
|
};
|
|
158
160
|
}
|
|
159
161
|
/**
|
|
@@ -173,11 +175,11 @@ function isBroaderGrant(a, b) {
|
|
|
173
175
|
*/
|
|
174
176
|
export function indexReachByScope(reach) {
|
|
175
177
|
const byScope = new Map();
|
|
176
|
-
for (const
|
|
177
|
-
if (!
|
|
178
|
+
for (const g of reach) {
|
|
179
|
+
if (!g?.scope)
|
|
178
180
|
continue;
|
|
179
|
-
const key = `${
|
|
180
|
-
const tag = toScopeGrantTag(
|
|
181
|
+
const key = `${g.scope.kind}:${g.scope.id}`;
|
|
182
|
+
const tag = toScopeGrantTag(g);
|
|
181
183
|
const existing = byScope.get(key);
|
|
182
184
|
if (!existing || isBroaderGrant(tag, existing))
|
|
183
185
|
byScope.set(key, tag);
|
|
@@ -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, 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,104 +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
|
-
}
|
|
149
|
-
/** ZIG-640 — server-side org rebind; existing OAuth Bearer keeps working. */
|
|
150
|
-
async function rebindDelegateOrg(creds, orgId) {
|
|
151
|
-
const url = `${getBackendUrl()}/agents/claude-delegate/rebind`;
|
|
152
|
-
const res = await fetch(url, {
|
|
153
|
-
method: 'POST',
|
|
154
|
-
headers: {
|
|
155
|
-
'content-type': 'application/json',
|
|
156
|
-
Authorization: `Bearer ${creds.operatorKey}`,
|
|
157
|
-
'X-Agent-Id': creds.agentId,
|
|
158
|
-
},
|
|
159
|
-
body: JSON.stringify({ orgId }),
|
|
160
|
-
});
|
|
161
|
-
const body = await res.text().catch(() => '');
|
|
162
|
-
if (!res.ok) {
|
|
163
|
-
throw new Error(`POST /agents/claude-delegate/rebind ${res.status} ${body.slice(0, 200)}`);
|
|
164
|
-
}
|
|
165
|
-
return body ? JSON.parse(body) : {};
|
|
166
|
-
}
|
|
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).
|
|
167
74
|
/** ZIG-640 — runtime acting org from server (self-hire / agent row). */
|
|
168
75
|
async function fetchDelegateAccess(creds) {
|
|
169
76
|
const url = `${getBackendUrl()}/agents/claude-delegate/access`;
|
|
@@ -182,7 +89,7 @@ async function fetchDelegateAccess(creds) {
|
|
|
182
89
|
}
|
|
183
90
|
/**
|
|
184
91
|
* ZIG-739 — the operator's full org membership (not just granted scopes, which
|
|
185
|
-
* is all
|
|
92
|
+
* is all ziggs_list_grants sees). Lets the delegate resolve an org name to
|
|
186
93
|
* an id and offer a pick-list instead of demanding a pasted org_... id.
|
|
187
94
|
*/
|
|
188
95
|
async function fetchMyOrgs(creds) {
|
|
@@ -224,64 +131,32 @@ function resolveOrgSelector(orgs, selector) {
|
|
|
224
131
|
return { status: 'not-found' };
|
|
225
132
|
}
|
|
226
133
|
/**
|
|
227
|
-
* ZIG-641 — cross-connection discovery
|
|
228
|
-
* grant
|
|
229
|
-
* connectionId/grantId no longer has to arrive out of
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* with proxyConnection.
|
|
134
|
+
* ZIG-641 / ZIG-648 — cross-connection discovery over the unified GET /grants:
|
|
135
|
+
* every connection grant this agent holds, grouped by connection so
|
|
136
|
+
* ziggs_connection_proxy's connectionId/grantId no longer has to arrive out of
|
|
137
|
+
* band. `provider` comes from the grant's resolved scope label. The response is
|
|
138
|
+
* scanned defensively for leaked secrets, as proxyConnection does.
|
|
233
139
|
*/
|
|
234
140
|
async function listConnectionsForHolder(creds) {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
parsed = body ? JSON.parse(body) : null;
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
parsed = body;
|
|
141
|
+
const client = new GrantsClient(creds.operatorKey, creds.agentId);
|
|
142
|
+
// All pages of the agent's live connection grants (not just the first page).
|
|
143
|
+
const items = await client.listAllGrants({
|
|
144
|
+
scopeKind: 'connection',
|
|
145
|
+
health: 'active',
|
|
146
|
+
});
|
|
147
|
+
const byConnection = new Map();
|
|
148
|
+
for (const g of items) {
|
|
149
|
+
const connectionId = g.scope.id;
|
|
150
|
+
let group = byConnection.get(connectionId);
|
|
151
|
+
if (!group) {
|
|
152
|
+
group = { connectionId, provider: g.scope.label ?? null, grants: [] };
|
|
153
|
+
byConnection.set(connectionId, group);
|
|
154
|
+
}
|
|
155
|
+
group.grants.push(g);
|
|
254
156
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
* ZIG-686 — agent-initiated MCP connection request: ask the principal to
|
|
259
|
-
* connect a remote MCP server and grant this agent the listed tools. Opens a
|
|
260
|
-
* connection-consent agreement in the working chat (ZIG-798): the human
|
|
261
|
-
* approves it there like any agreement, and on approval the server is connected
|
|
262
|
-
* (if needed) and this agent is granted the tools. Returns the agreement id.
|
|
263
|
-
*/
|
|
264
|
-
async function createMcpConnectionRequest(creds, input) {
|
|
265
|
-
const url = `${getBackendUrl()}/connections/mcp/requests`;
|
|
266
|
-
const res = await fetch(url, {
|
|
267
|
-
method: 'POST',
|
|
268
|
-
headers: {
|
|
269
|
-
'content-type': 'application/json',
|
|
270
|
-
Authorization: `Bearer ${creds.operatorKey}`,
|
|
271
|
-
'X-Agent-Id': creds.agentId,
|
|
272
|
-
},
|
|
273
|
-
body: JSON.stringify({
|
|
274
|
-
serverUrl: input.serverUrl,
|
|
275
|
-
tools: input.tools,
|
|
276
|
-
reason: input.reason,
|
|
277
|
-
chatId: input.chatId,
|
|
278
|
-
}),
|
|
279
|
-
});
|
|
280
|
-
const body = await res.text().catch(() => '');
|
|
281
|
-
if (!res.ok) {
|
|
282
|
-
throw new Error(`POST /connections/mcp/requests ${res.status} ${body.slice(0, 200)}`);
|
|
283
|
-
}
|
|
284
|
-
return body ? JSON.parse(body) : {};
|
|
157
|
+
const result = [...byConnection.values()];
|
|
158
|
+
assertNoLeakedConnectionSecret(JSON.stringify(result));
|
|
159
|
+
return result;
|
|
285
160
|
}
|
|
286
161
|
/**
|
|
287
162
|
* Ids this delegate answers for: its own agent id plus its principal's user
|
|
@@ -312,9 +187,21 @@ async function loadSessionActionsPayload(creds, cfg, opts) {
|
|
|
312
187
|
// failure so hasActiveWork:false is not mistaken for "no tasks".
|
|
313
188
|
activeTasksError = e.message;
|
|
314
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
|
+
}
|
|
315
200
|
return formatPendingDecisionsPayload(inbox, webOrigin, {
|
|
316
201
|
activeTasks,
|
|
317
202
|
activeTasksError,
|
|
203
|
+
paymentApprovals,
|
|
204
|
+
paymentApprovalsError,
|
|
318
205
|
withSessionCard: opts?.withSessionCard,
|
|
319
206
|
});
|
|
320
207
|
}
|
|
@@ -378,9 +265,9 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
378
265
|
? 'Not connected — complete MCP OAuth consent first, then re-check actingOrgId here.'
|
|
379
266
|
: actingOrgName
|
|
380
267
|
? `This MCP session acts in **${actingOrgName}** (${actingOrgId}). Runtime org comes from server-side delegate + self-hire — not the OAuth JWT.`
|
|
381
|
-
: 'Connected but could not resolve acting org name —
|
|
268
|
+
: 'Connected but could not resolve acting org name — reconnect MCP OAuth if needed.',
|
|
382
269
|
switchOrgHint: switchOrgHint ??
|
|
383
|
-
'
|
|
270
|
+
'Org is fixed at OAuth consent. Reconnect MCP OAuth and pick the target org on the consent screen to act elsewhere.',
|
|
384
271
|
apiBase: getBackendUrl(),
|
|
385
272
|
webAppOrigin: webOrigin,
|
|
386
273
|
docs: 'https://ziggsai.com/docs',
|
|
@@ -390,7 +277,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
390
277
|
: 'Next: ziggs_inbox or ziggs_pending_decisions at session start.',
|
|
391
278
|
});
|
|
392
279
|
});
|
|
393
|
-
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 () => {
|
|
394
281
|
try {
|
|
395
282
|
const orgs = await fetchMyOrgs(creds);
|
|
396
283
|
return textResult({ count: orgs.length, orgs });
|
|
@@ -399,41 +286,6 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
399
286
|
return toolError(e.message);
|
|
400
287
|
}
|
|
401
288
|
});
|
|
402
|
-
server.tool('ziggs_switch_org', 'Switch which org this MCP OAuth session acts in without reconnecting. Existing Bearer unchanged; runtime org flips server-side. Requires confirm=true (party-identity change). Accepts an org id (org_...) OR an org name — names resolve against your memberships (see ziggs_list_my_orgs); an ambiguous name returns the candidates. Call ziggs_auth_status after to verify actingOrgId.', {
|
|
403
|
-
org: z
|
|
404
|
-
.string()
|
|
405
|
-
.describe('Org id (org_...) or org name to act in — must be one you belong to'),
|
|
406
|
-
confirm: z
|
|
407
|
-
.literal(true)
|
|
408
|
-
.describe('Must be true — confirms the human approved switching acting org'),
|
|
409
|
-
}, WRITE, async ({ org, confirm }) => {
|
|
410
|
-
if (confirm !== true) {
|
|
411
|
-
return toolError('confirm must be true — org switch changes which workspace you act in');
|
|
412
|
-
}
|
|
413
|
-
const selector = org.trim();
|
|
414
|
-
try {
|
|
415
|
-
const orgs = await fetchMyOrgs(creds);
|
|
416
|
-
const resolved = resolveOrgSelector(orgs, selector);
|
|
417
|
-
if (resolved.status === 'ambiguous') {
|
|
418
|
-
return toolError(`"${selector}" matches ${resolved.matches.length} orgs — switch by exact orgId. Candidates: ${JSON.stringify(resolved.matches)}`);
|
|
419
|
-
}
|
|
420
|
-
if (resolved.status === 'not-found') {
|
|
421
|
-
return toolError(`No org matches "${selector}". You belong to: ${JSON.stringify(orgs)}`);
|
|
422
|
-
}
|
|
423
|
-
const result = await rebindDelegateOrg(creds, resolved.orgId);
|
|
424
|
-
return textResult({
|
|
425
|
-
ok: true,
|
|
426
|
-
resolvedOrgId: resolved.orgId,
|
|
427
|
-
...result,
|
|
428
|
-
note: result.unchanged
|
|
429
|
-
? 'Already acting in this org — no changes made.'
|
|
430
|
-
: 'Org rebind complete. Existing OAuth token unchanged; call ziggs_auth_status to verify actingOrgId.',
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
catch (e) {
|
|
434
|
-
return toolError(e.message);
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
289
|
server.tool('ziggs_pending_decisions', ZIGGS_PENDING_DECISIONS_DESCRIPTION, {}, READ_ONLY, async () => {
|
|
438
290
|
try {
|
|
439
291
|
const payload = await loadSessionActionsPayload(creds, cfg);
|
|
@@ -575,24 +427,26 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
575
427
|
return toolError(e.message);
|
|
576
428
|
}
|
|
577
429
|
});
|
|
578
|
-
server.tool('ziggs_propose_agreement', 'Propose a direct agreement to one counterparty in a chat
|
|
430
|
+
server.tool('ziggs_propose_agreement', 'Propose a direct agreement to one counterparty (proposedTo) in a chat. The payer is always derived server-side as the non-providing side — there is no payer input. 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). Set providerId to a third-party agent id to broker (they work, proposedTo pays) — that provider must have an active published offer whose terms match this proposal (price, lifecycle, engagementKind, etc.) or the call fails naming the mismatched field. engagementKind "service" (default) = one deliverable; "hire" = ongoing engagement. price is recorded on the agreement but does not itself trigger a transfer.', {
|
|
579
431
|
proposedTo: z.string(),
|
|
580
432
|
chatId: z.string(),
|
|
581
433
|
description: z.string(),
|
|
434
|
+
providerId: z
|
|
435
|
+
.string()
|
|
436
|
+
.optional()
|
|
437
|
+
.describe('Who does the work. Omitted = proposedTo (commission). Your agent id = offer. Another agent id = broker/matchmaking.'),
|
|
582
438
|
price: z.number().optional().describe('Optional; does not trigger transfer by itself'),
|
|
583
439
|
engagementKind: z
|
|
584
440
|
.enum(['hire', 'service'])
|
|
585
441
|
.optional()
|
|
586
442
|
.describe("'service' (default) = one-off deliverable; 'hire' = ongoing engagement"),
|
|
587
|
-
}, WRITE, async ({ proposedTo, chatId, description, price, engagementKind }) => {
|
|
443
|
+
}, WRITE, async ({ proposedTo, chatId, description, providerId, price, engagementKind }) => {
|
|
588
444
|
try {
|
|
589
445
|
const agreement = await proposeDirectTo({
|
|
590
446
|
proposedTo,
|
|
591
447
|
chatId,
|
|
592
448
|
description,
|
|
593
|
-
|
|
594
|
-
// and the payer is derived server-side as the creator's side.
|
|
595
|
-
providerId: proposedTo,
|
|
449
|
+
providerId: providerId?.trim() || proposedTo,
|
|
596
450
|
price,
|
|
597
451
|
engagementKind: engagementKind ?? 'service',
|
|
598
452
|
}, creds);
|
|
@@ -743,6 +597,49 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
743
597
|
return toolError(e.message);
|
|
744
598
|
}
|
|
745
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
|
+
});
|
|
746
643
|
server.tool('ziggs_inbox', ZIGGS_INBOX_DESCRIPTION, {
|
|
747
644
|
ack: z
|
|
748
645
|
.array(z.object({
|
|
@@ -768,13 +665,18 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
768
665
|
activeTasksError = e.message;
|
|
769
666
|
}
|
|
770
667
|
// ZIG-635: tag each scope with the covering grant from the caller's own
|
|
771
|
-
//
|
|
668
|
+
// held context grants. Best-effort — the inbox still works untagged.
|
|
669
|
+
// All pages of live grants only (GET /grants also returns expired/revoked
|
|
670
|
+
// and paginates), so a scope isn't left untagged behind the first page.
|
|
772
671
|
let reach = [];
|
|
773
672
|
try {
|
|
774
|
-
reach = await new
|
|
673
|
+
reach = await new GrantsClient(creds.operatorKey, creds.agentId).listAllGrants({
|
|
674
|
+
scopeKind: ['chat', 'agreement', 'org'],
|
|
675
|
+
health: 'active',
|
|
676
|
+
});
|
|
775
677
|
}
|
|
776
678
|
catch {
|
|
777
|
-
// omit grant tags when
|
|
679
|
+
// omit grant tags when the grants read fails
|
|
778
680
|
}
|
|
779
681
|
return textResult(formatInboxToolResult(inbox, acked, cfg.ZIGGS_WEB_URL, activeTasks, reach, activeTasksError));
|
|
780
682
|
}
|
|
@@ -782,17 +684,61 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
782
684
|
return toolError(e.message);
|
|
783
685
|
}
|
|
784
686
|
});
|
|
785
|
-
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).'),
|
|
696
|
+
cursor: z
|
|
697
|
+
.string()
|
|
698
|
+
.optional()
|
|
699
|
+
.describe('Opaque cursor from a prior nextCursor'),
|
|
700
|
+
limit: z.number().optional().describe('Page size (default server-side)'),
|
|
701
|
+
}, READ_ONLY, async ({ scopeKind, health, cursor, limit }) => {
|
|
786
702
|
try {
|
|
787
|
-
const client = new
|
|
788
|
-
const
|
|
789
|
-
|
|
703
|
+
const client = new GrantsClient(creds.operatorKey, creds.agentId);
|
|
704
|
+
const kinds = scopeKind && scopeKind.length
|
|
705
|
+
? scopeKind
|
|
706
|
+
: undefined;
|
|
707
|
+
const { items, nextCursor } = await client.listGrants({
|
|
708
|
+
scopeKind: kinds,
|
|
709
|
+
// Reach = live grants only by default; pass health for expired/revoked.
|
|
710
|
+
health: health ?? 'active',
|
|
711
|
+
cursor,
|
|
712
|
+
limit,
|
|
713
|
+
});
|
|
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
|
+
});
|
|
790
723
|
}
|
|
791
724
|
catch (e) {
|
|
792
725
|
return toolError(e.message);
|
|
793
726
|
}
|
|
794
727
|
});
|
|
795
|
-
server.tool('
|
|
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));
|
|
736
|
+
}
|
|
737
|
+
catch (e) {
|
|
738
|
+
return toolError(e.message);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
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 () => {
|
|
796
742
|
try {
|
|
797
743
|
const client = new ContextDiscoveryClient(creds.operatorKey, creds.agentId);
|
|
798
744
|
const items = await client.discoverGrantable();
|
|
@@ -859,14 +805,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
859
805
|
if ((chatId && agreementId) || (!chatId && !agreementId)) {
|
|
860
806
|
return toolError('Pass exactly one of chatId or agreementId');
|
|
861
807
|
}
|
|
862
|
-
const { artifactId } = await
|
|
863
|
-
text,
|
|
864
|
-
visibility,
|
|
865
|
-
chatId,
|
|
866
|
-
agreementId,
|
|
867
|
-
taskId,
|
|
868
|
-
content_type,
|
|
869
|
-
});
|
|
808
|
+
const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId).writeStrict({ text, visibility, chatId, agreementId, taskId, content_type });
|
|
870
809
|
return textResult({
|
|
871
810
|
ok: true,
|
|
872
811
|
artifactId,
|
|
@@ -988,12 +927,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
988
927
|
.describe('Action-specific arguments (provider-defined)'),
|
|
989
928
|
}, WRITE, async ({ connectionId, grantId, action, payload }) => {
|
|
990
929
|
try {
|
|
991
|
-
const result = await
|
|
992
|
-
connectionId,
|
|
993
|
-
grantId,
|
|
994
|
-
action,
|
|
995
|
-
payload,
|
|
996
|
-
});
|
|
930
|
+
const result = await new ConnectionsClient(creds.operatorKey, creds.agentId).proxy({ connectionId, grantId, action, payload });
|
|
997
931
|
return textResult({ ok: true, action, result });
|
|
998
932
|
}
|
|
999
933
|
catch (e) {
|
|
@@ -1001,7 +935,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
1001
935
|
}
|
|
1002
936
|
});
|
|
1003
937
|
server.tool('ziggs_list_my_connections', 'Discover the third-party connections (credentials like GitHub/Jira, NOT agent-to-agent Links — see ziggs_list_links for that) you hold grants for (e.g. "is GitHub connected?") without the owner sharing connectionId/grantId out of band. ' +
|
|
1004
|
-
'Returns, per connection: connectionId, provider,
|
|
938
|
+
'Returns, per connection: connectionId, provider, and the grant(s) you hold — each as the canonical grant shape (grantId, scope, caveats, and grant health active/expired/revoked). ' +
|
|
1005
939
|
'Read-only — never returns credential material. Feed the connectionId + a grantId with health "active" into ziggs_connection_proxy to actually use it.', {}, READ_ONLY, async () => {
|
|
1006
940
|
try {
|
|
1007
941
|
const connections = await listConnectionsForHolder(creds);
|
|
@@ -1027,12 +961,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
1027
961
|
.describe('Plain-language reason shown to the human deciding'),
|
|
1028
962
|
}, WRITE, async ({ chatId, serverUrl, tools, reason }) => {
|
|
1029
963
|
try {
|
|
1030
|
-
const result = await
|
|
1031
|
-
chatId,
|
|
1032
|
-
serverUrl,
|
|
1033
|
-
tools,
|
|
1034
|
-
reason,
|
|
1035
|
-
});
|
|
964
|
+
const result = await new ConnectionsClient(creds.operatorKey, creds.agentId).requestMcpConnection({ chatId, serverUrl, tools, reason });
|
|
1036
965
|
return textResult({
|
|
1037
966
|
ok: true,
|
|
1038
967
|
...result,
|
|
@@ -1045,4 +974,5 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
1045
974
|
}
|
|
1046
975
|
});
|
|
1047
976
|
registerTrustTools(server, creds, cfg);
|
|
977
|
+
registerPaymentTools(server, creds);
|
|
1048
978
|
}
|
package/dist/trustTools.js
CHANGED
|
@@ -57,7 +57,23 @@ export function registerTrustTools(server, creds, cfg) {
|
|
|
57
57
|
return toolError(e.message);
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
|
-
server.tool('
|
|
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
|
+
});
|
|
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,
|
|
63
79
|
scopeId: z.string().describe('chatId, agreementId, or orgId'),
|
|
@@ -210,7 +226,7 @@ export function registerTrustTools(server, creds, cfg) {
|
|
|
210
226
|
const { agreement } = await claimAgreement(agreementId, creds);
|
|
211
227
|
return textResult({
|
|
212
228
|
status: 'linked',
|
|
213
|
-
message: 'Link invite claimed — you are now linked. A link is reach-only:
|
|
229
|
+
message: 'Link invite claimed — you are now linked. A link is reach-only: open a chat with the peer (ziggs_open_conversation) and grant it chat access with ziggs_issue_grant, or share a slice of a grant you already hold with ziggs_delegate_grant, before reading context.',
|
|
214
230
|
agreement,
|
|
215
231
|
});
|
|
216
232
|
}
|
|
@@ -252,7 +268,7 @@ export function registerTrustTools(server, creds, cfg) {
|
|
|
252
268
|
links: summaries,
|
|
253
269
|
...(hasActive
|
|
254
270
|
? {
|
|
255
|
-
nextSteps: 'A link is reach-only.
|
|
271
|
+
nextSteps: 'A link is reach-only. Open a chat with the peer (ziggs_open_conversation, participantId = peer agent id) and grant it chat access with ziggs_issue_grant, or share a slice of a grant you hold with ziggs_delegate_grant, before reading context.',
|
|
256
272
|
}
|
|
257
273
|
: {}),
|
|
258
274
|
});
|
|
@@ -279,7 +295,7 @@ export function registerTrustTools(server, creds, cfg) {
|
|
|
279
295
|
return toolError(e.message);
|
|
280
296
|
}
|
|
281
297
|
});
|
|
282
|
-
server.tool('ziggs_revoke_grant', 'Revoke a context grant and its descendants (DELETE /context/grants/:id). You can revoke (narrow) any grant you hold — this needs no special scope. Revoking a grant you issued or
|
|
298
|
+
server.tool('ziggs_revoke_grant', 'Revoke a context grant and its descendants (DELETE /context/grants/:id). You can revoke (narrow) any grant you hold — this needs no special scope. Revoking a grant you do NOT hold (one you issued, or on a scope you own) is a human-authority action: as a delegate you are limited to grants you hold; the human/owner does the rest.', {
|
|
283
299
|
grantId: z.string(),
|
|
284
300
|
}, DESTRUCTIVE, async ({ grantId }) => {
|
|
285
301
|
try {
|
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
|
@@ -38,14 +38,14 @@ The sections below elaborate this protocol with tools, examples, and edge cases.
|
|
|
38
38
|
|
|
39
39
|
## Session start — pending decisions + inbox
|
|
40
40
|
|
|
41
|
-
1. Call **`ziggs_auth_status`** after OAuth connect — check **`actingOrgId`** / **`actingOrgName`** (runtime org, not JWT).
|
|
42
|
-
2. To
|
|
41
|
+
1. Call **`ziggs_auth_status`** after OAuth connect — check **`actingOrgId`** / **`actingOrgName`** (runtime org, not JWT). Org is fixed at consent (ZIG-852).
|
|
42
|
+
2. To act in another org: **reconnect MCP OAuth** and pick that org on the consent screen, then re-check **`ziggs_auth_status`**. Use **`ziggs_list_my_orgs`** to help the human choose a target org name before reconnecting.
|
|
43
43
|
3. Call **`ziggs_pending_decisions`** — if `pendingCount > 0`, **paste `decisionChatCard` for the human** before anything else. Wait for explicit approve/reject; then `ziggs_respond_to_agreement`.
|
|
44
44
|
3. Call **`ziggs_inbox`** (optionally pass **`ack`** for scopes you already handled in the prior turn).
|
|
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
|