@ziggs-ai/api-client 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/ConnectionManager.d.ts +13 -1
  2. package/dist/ConnectionManager.js +52 -9
  3. package/dist/capabilities/artifacts.d.ts +3 -0
  4. package/dist/capabilities/artifacts.js +94 -0
  5. package/dist/capabilities/chat.d.ts +11 -0
  6. package/dist/capabilities/chat.js +38 -0
  7. package/dist/capabilities/connections.d.ts +4 -0
  8. package/dist/capabilities/connections.js +112 -0
  9. package/dist/capabilities/context.d.ts +23 -0
  10. package/dist/capabilities/context.js +220 -0
  11. package/dist/capabilities/discovery.d.ts +4 -0
  12. package/dist/capabilities/discovery.js +77 -0
  13. package/dist/capabilities/grants.d.ts +9 -0
  14. package/dist/capabilities/grants.js +77 -0
  15. package/dist/capabilities/index.d.ts +9 -0
  16. package/dist/capabilities/index.js +9 -0
  17. package/dist/capabilities/links.d.ts +17 -0
  18. package/dist/capabilities/links.js +219 -0
  19. package/dist/capabilities/payments.d.ts +11 -0
  20. package/dist/capabilities/payments.js +404 -0
  21. package/dist/capabilities/types.d.ts +88 -0
  22. package/dist/capabilities/types.js +24 -0
  23. package/dist/http/AgreementClient.d.ts +6 -0
  24. package/dist/http/ConnectionsClient.d.ts +27 -1
  25. package/dist/http/ConnectionsClient.js +29 -0
  26. package/dist/http/ContextReadClient.js +7 -1
  27. package/dist/http/GrantsClient.d.ts +16 -0
  28. package/dist/http/GrantsClient.js +3 -0
  29. package/dist/http/InboxClient.d.ts +95 -65
  30. package/dist/http/InboxClient.js +42 -14
  31. package/dist/http/MarketplaceClient.d.ts +8 -0
  32. package/dist/http/OrgsClient.d.ts +36 -0
  33. package/dist/http/OrgsClient.js +61 -0
  34. package/dist/http/PaymentsClient.d.ts +75 -10
  35. package/dist/http/PaymentsClient.js +26 -14
  36. package/dist/http/index.d.ts +6 -6
  37. package/dist/http/index.js +1 -1
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +1 -0
  40. package/package.json +1 -1
  41. package/dist/http/grantRails.d.ts +0 -20
  42. package/dist/http/grantRails.js +0 -50
@@ -0,0 +1,219 @@
1
+ import { createAgreement, claimAgreement, listAgreements, revokeAgreement, } from '../http/AgreementClient.js';
2
+ import { fullCreds } from './types.js';
3
+ const DEFAULT_WEB_URL = 'https://ziggsai.com';
4
+ /** Public Streamable-HTTP MCP endpoint; OAuth is discovered from it (RFC 9728). */
5
+ const ZIGGS_MCP_URL = 'https://mcp.ziggsai.com/mcp';
6
+ function webAppOrigin(env) {
7
+ return (env.webUrl?.trim() || DEFAULT_WEB_URL).replace(/\/$/, '');
8
+ }
9
+ /**
10
+ * The second form an invite travels in: text the recipient pastes into their
11
+ * own assistant, which then connects itself and claims the invite. The claim
12
+ * URL alone only helps someone who already has Ziggs and an assistant wired
13
+ * up, so hand the caller both and let it pick per recipient.
14
+ */
15
+ function invitePasteText(agreementId, claimUrl) {
16
+ return [
17
+ 'Connect me to Ziggs and accept this agent link invite.',
18
+ '',
19
+ `1. Add this MCP server: ${ZIGGS_MCP_URL}`,
20
+ ' It speaks Streamable HTTP and uses OAuth — pasting the URL is enough,',
21
+ ' but I may need to approve a consent screen in my browser.',
22
+ `2. Once connected, call the tool ziggs_claim_link_invite with agreementId "${agreementId}".`,
23
+ '3. Then tell me who I am linked with, and what they can and cannot see.',
24
+ '',
25
+ 'If you cannot add MCP servers yourself, tell me exactly where to paste that',
26
+ "URL in my assistant's settings, then continue from step 2.",
27
+ '',
28
+ `Invite link (if you need it in a browser instead): ${claimUrl}`,
29
+ ].join('\n');
30
+ }
31
+ /**
32
+ * ZIG-670 — link-shaped summaries, not raw agreement documents: the money
33
+ * block, approvals array, and Mongo internals are noise on a trust
34
+ * relationship. ZIG-956 moved this into the shared layer so the MCP mutations
35
+ * return it too (they used to leak the raw agreement doc).
36
+ */
37
+ export function linkSummary(a) {
38
+ return {
39
+ agreementId: a.agreementId,
40
+ status: a.status,
41
+ proposalStatus: a.proposalStatus,
42
+ parties: {
43
+ creatorAgent: a.parties?.creatorAgent ?? null,
44
+ providerAgent: a.parties?.providerAgent ?? null,
45
+ creator: a.parties?.creator ?? null,
46
+ proposedTo: a.parties?.proposedTo ?? null,
47
+ },
48
+ ...(a.description ? { description: a.description } : {}),
49
+ createdAt: a.createdAt,
50
+ };
51
+ }
52
+ /** A link is reach-only — the follow-up move differs by surface tool names. */
53
+ export function linkIsReachOnly(env) {
54
+ return env.surface === 'mcp'
55
+ ? '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.'
56
+ : 'A link is reach-only: it does not grant context. Share a slice of a grant you hold with context_delegate, or ask the peer owner to issue one, before reading.';
57
+ }
58
+ const LINK_STATUSES = ['active', 'open', 'cancelled', 'all'];
59
+ export const requestLinkCapability = {
60
+ key: 'request_link',
61
+ names: { sdk: 'request_link', mcp: 'ziggs_request_link' },
62
+ descriptions: {
63
+ sdk: 'Request a bilateral trust link with another agent before cross-org reach (party-to-party, NOT a third-party service connection — that is the connection rail). A link is just an agreement (engagementKind "link"). The target OWNER must approve it before unpublished delegates can message each other.',
64
+ mcp: 'Request a bilateral trust link with another agent before cross-org reach (party-to-party, NOT a third-party service connection — see ziggs_list_my_connections for that). A link is just an agreement (POST /agreements {engagementKind:"link"}). The target OWNER must approve it (via ziggs_respond_to_agreement) before unpublished delegates can message each other.',
65
+ },
66
+ annotation: 'write',
67
+ params: {
68
+ providerId: {
69
+ type: 'string',
70
+ required: true,
71
+ description: 'Bare agent id to link with (the target delegate). Use agent search or a known delegate id — do not guess.',
72
+ },
73
+ message: {
74
+ type: 'string',
75
+ description: 'Optional note shown to the counterparty human on approval (agreement description)',
76
+ },
77
+ },
78
+ needsAgentId: true,
79
+ handler: async (args, env) => {
80
+ const { agreement } = await createAgreement({
81
+ engagementKind: 'link',
82
+ providerId: args['providerId'],
83
+ description: args['message'],
84
+ }, fullCreds(env));
85
+ return {
86
+ status: 'pending',
87
+ message: env.surface === 'mcp'
88
+ ? 'Link agreement created — the counterparty owner must approve (ziggs_respond_to_agreement) before cross-org reach. Surface pending state to the human.'
89
+ : 'Link agreement created — the counterparty owner must approve (agreement_respond on their side) before cross-org reach. Surface pending state to your principal.',
90
+ agreement: linkSummary(agreement),
91
+ };
92
+ },
93
+ sdkOptions: { isAgreementCreation: true },
94
+ };
95
+ export const createLinkInviteCapability = {
96
+ key: 'create_link_invite',
97
+ names: { sdk: 'create_link_invite', mcp: 'ziggs_create_link_invite' },
98
+ descriptions: {
99
+ sdk: "Create a shareable OPEN link invite (bilateral agent-to-agent trust) when you do NOT have the counterparty's agent id (e.g. connecting across orgs). Creates an open link agreement proposed to everyone; the recipient forms the link by claiming the returned inviteId.",
100
+ mcp: 'Create a shareable OPEN link invite (bilateral agent-to-agent trust, NOT a third-party service connection — see ziggs_list_my_connections for that) when you do NOT have the counterparty\'s agent id (e.g. connecting across orgs). Creates an open link agreement (POST /agreements {engagementKind:"link"}, proposedTo:"everyone"). Share the returned inviteId (agreementId) out-of-band; the recipient forms the link by calling ziggs_claim_link_invite — neither side pastes an agent id. Revoke via ziggs_revoke_link to disable.',
101
+ },
102
+ annotation: 'write',
103
+ params: {
104
+ message: {
105
+ type: 'string',
106
+ description: 'Optional note shown to whoever opens the invite (agreement description)',
107
+ },
108
+ },
109
+ needsAgentId: true,
110
+ handler: async (args, env) => {
111
+ const { agreement } = await createAgreement({ engagementKind: 'link', description: args['message'] }, fullCreds(env));
112
+ const claimUrl = `${webAppOrigin(env)}/app/link-invites/${agreement.agreementId}`;
113
+ return {
114
+ status: 'open',
115
+ inviteId: agreement.agreementId,
116
+ claimUrl,
117
+ pasteText: invitePasteText(agreement.agreementId, claimUrl),
118
+ message: env.surface === 'mcp'
119
+ ? 'Open link invite created, single-use and valid 7 days. Give the human BOTH forms and say which is which: claimUrl for a recipient who already uses Ziggs, pasteText for one who has an AI assistant but no Ziggs account — pasting it makes their assistant connect and claim the invite itself. No agent id needed on either side.'
120
+ : 'Open link invite created, single-use and valid 7 days. Share claimUrl with a counterparty who already uses Ziggs, or pasteText with one who has an assistant but no Ziggs account yet — their assistant connects and claims it. No agent id needed on either side. Revoke with revoke_link to disable.',
121
+ agreement: linkSummary(agreement),
122
+ };
123
+ },
124
+ sdkOptions: { isAgreementCreation: true },
125
+ };
126
+ export const claimLinkInviteCapability = {
127
+ key: 'claim_link_invite',
128
+ names: { sdk: 'claim_link_invite', mcp: 'ziggs_claim_link_invite' },
129
+ descriptions: {
130
+ sdk: 'Claim an open link invite by its id to form a bilateral link. You become the counterparty and the link activates immediately (cross-org reach). You cannot claim your own invite.',
131
+ mcp: 'Claim an open link invite by its id to form a bilateral link (agent-to-agent trust, NOT a third-party service connection — see ziggs_list_my_connections for that) (POST /agreements/:id/claim). You become the counterparty and the link activates immediately (cross-org reach + bilateral context grants). You cannot claim your own invite.',
132
+ },
133
+ annotation: 'write',
134
+ params: {
135
+ agreementId: {
136
+ type: 'string',
137
+ required: true,
138
+ description: 'The invite id (agreementId) shared by the issuer',
139
+ },
140
+ },
141
+ needsAgentId: true,
142
+ handler: async (args, env) => {
143
+ const { agreement } = await claimAgreement(args['agreementId'], fullCreds(env));
144
+ return {
145
+ status: 'linked',
146
+ message: `Link invite claimed — you are now linked. ${linkIsReachOnly(env)}`,
147
+ agreement: linkSummary(agreement),
148
+ };
149
+ },
150
+ };
151
+ export const listLinksCapability = {
152
+ key: 'list_links',
153
+ names: { sdk: 'list_links', mcp: 'ziggs_list_links' },
154
+ descriptions: {
155
+ sdk: 'List link agreements for this agent — bilateral agent-to-agent trust relationships (GET /agreements?engagementKind=link). Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties.',
156
+ mcp: 'List link agreements for this delegate — bilateral agent-to-agent trust relationships, NOT third-party service connections (see ziggs_list_my_connections for those) (GET /agreements?engagementKind=link). Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties.creatorAgent (requester), parties.providerAgent (target), parties.proposedTo (target owner). Approve pending links via ziggs_respond_to_agreement.',
157
+ },
158
+ annotation: 'read-only',
159
+ params: {
160
+ status: {
161
+ type: 'string',
162
+ enum: LINK_STATUSES,
163
+ description: 'active (default) = established links; open = pending proposals/invites awaiting approval or claim; cancelled = revoked; all = every link regardless of status',
164
+ },
165
+ },
166
+ needsAgentId: true,
167
+ handler: async (args, env) => {
168
+ const status = args['status'] ?? 'active';
169
+ if (!LINK_STATUSES.includes(status)) {
170
+ throw new Error('status must be one of active, open, cancelled, all');
171
+ }
172
+ const links = await listAgreements({
173
+ engagementKind: 'link',
174
+ ...(status === 'all' ? {} : { status }),
175
+ }, fullCreds(env));
176
+ const summaries = links.map(linkSummary);
177
+ const hasActive = links.some((a) => a.status === 'active');
178
+ return {
179
+ count: summaries.length,
180
+ status,
181
+ links: summaries,
182
+ ...(hasActive ? { nextSteps: linkIsReachOnly(env) } : {}),
183
+ };
184
+ },
185
+ sdkOptions: { isGenericFallback: true },
186
+ };
187
+ export const revokeLinkCapability = {
188
+ key: 'revoke_link',
189
+ names: { sdk: 'revoke_link', mcp: 'ziggs_revoke_link' },
190
+ descriptions: {
191
+ sdk: 'Revoke a bilateral link agreement (DELETE /agreements/:agreementId). Either party may revoke; cross-org reach ends immediately. For non-link agreements (hire/service/quest), use agreement_revoke — same endpoint, different messaging.',
192
+ mcp: 'Revoke a bilateral link agreement — agent-to-agent trust, NOT a third-party service connection (DELETE /agreements/:agreementId). Either party may revoke; cross-org reach ends immediately. For non-link agreements (hire/service/quest), use ziggs_revoke_agreement — same endpoint, different messaging.',
193
+ },
194
+ annotation: 'destructive',
195
+ params: {
196
+ agreementId: {
197
+ type: 'string',
198
+ required: true,
199
+ description: 'agreementId of the link agreement (from the link lister)',
200
+ },
201
+ },
202
+ needsAgentId: true,
203
+ handler: async (args, env) => {
204
+ const result = await revokeAgreement(args['agreementId'], fullCreds(env));
205
+ return {
206
+ status: 'revoked',
207
+ message: 'Link revoked — unpublished cross-org reach to this peer is blocked again.',
208
+ agreementId: args['agreementId'],
209
+ agreement: result.agreement ? linkSummary(result.agreement) : null,
210
+ };
211
+ },
212
+ };
213
+ export const LINK_CAPABILITIES = [
214
+ requestLinkCapability,
215
+ createLinkInviteCapability,
216
+ claimLinkInviteCapability,
217
+ listLinksCapability,
218
+ revokeLinkCapability,
219
+ ];
@@ -0,0 +1,11 @@
1
+ import { type CapabilityDefinition } from './types.js';
2
+ export declare const paymentBalanceCapability: CapabilityDefinition;
3
+ export declare const paymentResolveWalletCapability: CapabilityDefinition;
4
+ export declare const paymentTransferCapability: CapabilityDefinition;
5
+ export declare const paymentWaitForApprovalCapability: CapabilityDefinition;
6
+ export declare const paymentHoldCapability: CapabilityDefinition;
7
+ export declare const paymentReleaseCapability: CapabilityDefinition;
8
+ export declare const paymentIssueGrantCapability: CapabilityDefinition;
9
+ export declare const paymentAttenuateGrantCapability: CapabilityDefinition;
10
+ export declare const paymentRevokeGrantCapability: CapabilityDefinition;
11
+ export declare const PAYMENT_CAPABILITIES: CapabilityDefinition[];
@@ -0,0 +1,404 @@
1
+ import { PaymentsClient } from '../http/PaymentsClient.js';
2
+ import { rethrowWithContext, } from './types.js';
3
+ function client(env) {
4
+ const { operatorKey, agentId } = env.creds;
5
+ if (!operatorKey)
6
+ throw new Error('operatorKey missing from tool context');
7
+ return new PaymentsClient(operatorKey, agentId, env.baseUrl);
8
+ }
9
+ /** Lower the optional caveat args to the wire's caveat list (was written twice). */
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
+ const grantCaveatParams = {
23
+ maxAmount: { type: 'number', description: 'Per-transfer ceiling in cents' },
24
+ dailyBudget: { type: 'number', description: 'Rolling daily budget in cents' },
25
+ allowedRecipients: {
26
+ type: 'array',
27
+ items: { type: 'string' },
28
+ description: 'Wallet ids the holder may pay',
29
+ },
30
+ expiresInSeconds: { type: 'number', description: 'Grant lifetime from now' },
31
+ };
32
+ export const paymentBalanceCapability = {
33
+ key: 'payment_balance',
34
+ names: { sdk: 'payment_balance', mcp: 'ziggs_payment_balance' },
35
+ descriptions: {
36
+ sdk: "Check the caller's current wallet balance and available balance (total minus active holds). Use before a transfer to confirm sufficient funds.",
37
+ mcp: "Check the caller's current wallet balance and available balance (total minus active holds). Use before a transfer to confirm sufficient funds.",
38
+ },
39
+ annotation: 'read-only',
40
+ params: {},
41
+ handler: async (_args, env) => {
42
+ try {
43
+ return await client(env).balance();
44
+ }
45
+ catch (e) {
46
+ rethrowWithContext(e, 'Failed to get balance');
47
+ }
48
+ },
49
+ };
50
+ export const paymentResolveWalletCapability = {
51
+ key: 'payment_resolve_wallet',
52
+ names: { sdk: 'payment_resolve_wallet', mcp: 'ziggs_payment_resolve_wallet' },
53
+ descriptions: {
54
+ sdk: 'Look up a walletId by userId or agentId. Use before a transfer when you only know the recipient by their platform ID.',
55
+ mcp: 'Look up a walletId by userId or agentId. Use before a transfer when you only know the recipient by their platform ID.',
56
+ },
57
+ annotation: 'read-only',
58
+ params: {
59
+ userId: { type: 'string', description: 'User to resolve' },
60
+ agentId: { type: 'string', description: 'Agent to resolve' },
61
+ },
62
+ handler: async (args, env) => {
63
+ if (!args['userId'] && !args['agentId'])
64
+ throw new Error('Provide userId or agentId to resolve a wallet');
65
+ try {
66
+ const wallet = await client(env).resolve({
67
+ userId: args['userId'],
68
+ agentId: args['agentId'],
69
+ });
70
+ return {
71
+ walletId: wallet?.walletId || null,
72
+ ownerId: wallet?.ownerId || null,
73
+ currency: wallet?.currency || 'pez',
74
+ status: wallet?.status || null,
75
+ };
76
+ }
77
+ catch (e) {
78
+ rethrowWithContext(e, 'Failed to resolve wallet');
79
+ }
80
+ },
81
+ };
82
+ export const paymentTransferCapability = {
83
+ key: 'payment_transfer',
84
+ names: { sdk: 'payment_transfer', mcp: 'ziggs_payment_transfer' },
85
+ descriptions: {
86
+ sdk: 'Transfer funds to another wallet. Amounts are integer cents. Transfers above the wallet owner\'s policy pause with status "approval_required" — wait inline with payment_wait_for_approval when you expect a quick decision.',
87
+ mcp: '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.',
88
+ },
89
+ annotation: 'write',
90
+ params: {
91
+ toWalletId: {
92
+ type: 'string',
93
+ required: true,
94
+ description: 'Destination wal_... id — or a userId/agentId to auto-resolve',
95
+ },
96
+ amount: { type: 'number', required: true, description: 'Amount in integer cents, > 0' },
97
+ description: { type: 'string', description: 'Human-readable transfer memo' },
98
+ idempotencyKey: {
99
+ type: 'string',
100
+ description: 'Client-supplied key to make retries safe (auto-generated when omitted)',
101
+ },
102
+ paymentGrantId: {
103
+ type: 'string',
104
+ description: 'Payment grant to spend under (required for agent-impersonated transfers)',
105
+ },
106
+ },
107
+ handler: async (args, env) => {
108
+ if (!args['toWalletId'])
109
+ throw new Error('toWalletId is required');
110
+ const amount = args['amount'];
111
+ if (!amount || amount <= 0)
112
+ throw new Error('amount must be positive');
113
+ let result;
114
+ try {
115
+ result = await client(env).transfer({
116
+ to: args['toWalletId'],
117
+ amount: Math.round(amount),
118
+ description: args['description'] || 'Agent-initiated transfer',
119
+ idempotencyKey: args['idempotencyKey'],
120
+ paymentGrantId: args['paymentGrantId'],
121
+ });
122
+ }
123
+ catch (e) {
124
+ rethrowWithContext(e, 'Transfer failed');
125
+ }
126
+ if (result.status === 'approval_required') {
127
+ const base = {
128
+ status: 'approval_required',
129
+ approvalId: result.approvalId,
130
+ expiresAt: result.expiresAt,
131
+ reason: result.reason,
132
+ amount,
133
+ toWalletId: result.toWalletId,
134
+ };
135
+ // Same pause, surface-local guidance: the MCP delegate must surface the
136
+ // pending approval to the human (pull-only MCP has no push); the SDK
137
+ // runtime routes via structured next_actions.
138
+ if (env.surface === 'mcp') {
139
+ return {
140
+ ...base,
141
+ 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). ' +
142
+ 'Wait inline with ziggs_payment_wait_for_approval when you expect a quick decision (≤2 min).',
143
+ };
144
+ }
145
+ return {
146
+ ...base,
147
+ message: 'Transfer paused: the wallet owner must approve this amount.',
148
+ next_actions: [
149
+ {
150
+ tool: 'payment_wait_for_approval',
151
+ when: 'You expect a quick decision (≤2 min) and can wait inline.',
152
+ args: { approvalId: result.approvalId, timeoutMs: 120000 },
153
+ },
154
+ {
155
+ tool: 'task_update_plan_step',
156
+ when: 'You want to abandon the transfer and route around it.',
157
+ },
158
+ ],
159
+ };
160
+ }
161
+ return {
162
+ status: 'transferred',
163
+ transactionId: result.transactionId,
164
+ amount,
165
+ toWalletId: result.toWalletId,
166
+ };
167
+ },
168
+ };
169
+ export const paymentWaitForApprovalCapability = {
170
+ key: 'payment_wait_for_approval',
171
+ names: { sdk: 'payment_wait_for_approval', mcp: 'ziggs_payment_wait_for_approval' },
172
+ descriptions: {
173
+ sdk: 'Poll a paused transfer (status "approval_required") until the human decides or the timeout passes. Returns executed | rejected | expired | timeout | gone.',
174
+ mcp: '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.',
175
+ },
176
+ annotation: 'read-only',
177
+ params: {
178
+ approvalId: {
179
+ type: 'string',
180
+ required: true,
181
+ description: 'Approval to wait on (from the paused transfer)',
182
+ },
183
+ timeoutMs: { type: 'number', description: 'Max wait, default 120000' },
184
+ pollMs: { type: 'number', description: 'Poll interval, default 3000 (min 500)' },
185
+ },
186
+ handler: async (args, env) => {
187
+ if (!args['approvalId'])
188
+ throw new Error('approvalId is required');
189
+ try {
190
+ const result = await client(env).waitForApproval(args['approvalId'], {
191
+ timeoutMs: args['timeoutMs'],
192
+ pollMs: args['pollMs'],
193
+ });
194
+ const loose = result;
195
+ return {
196
+ status: result.status,
197
+ approvalId: args['approvalId'],
198
+ transactionId: loose['transactionId'] || null,
199
+ approval: loose['approval'] || null,
200
+ };
201
+ }
202
+ catch (e) {
203
+ rethrowWithContext(e, 'wait_for_approval failed');
204
+ }
205
+ },
206
+ };
207
+ export const paymentHoldCapability = {
208
+ key: 'payment_hold',
209
+ names: { sdk: 'payment_hold', mcp: 'ziggs_payment_hold' },
210
+ descriptions: {
211
+ sdk: 'Pre-authorize (escrow) funds without moving them. Use to reserve payment at agreement formation; release with payment_release once work is complete or to refund if work is cancelled.',
212
+ mcp: "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.",
213
+ },
214
+ annotation: 'write',
215
+ params: {
216
+ amount: { type: 'number', required: true, description: 'Amount in integer cents, > 0' },
217
+ description: { type: 'string', description: 'Human-readable hold memo' },
218
+ idempotencyKey: { type: 'string', description: 'Client-supplied retry-safety key' },
219
+ },
220
+ handler: async (args, env) => {
221
+ const amount = args['amount'];
222
+ if (!amount || amount <= 0)
223
+ throw new Error('amount must be positive');
224
+ try {
225
+ const result = await client(env).hold({
226
+ amount: Math.round(amount),
227
+ description: args['description'] || 'Agent escrow hold',
228
+ idempotencyKey: args['idempotencyKey'],
229
+ });
230
+ return {
231
+ status: 'held',
232
+ transactionId: result.transaction?.transactionId || null,
233
+ amount,
234
+ };
235
+ }
236
+ catch (e) {
237
+ rethrowWithContext(e, 'Hold failed');
238
+ }
239
+ },
240
+ };
241
+ export const paymentReleaseCapability = {
242
+ key: 'payment_release',
243
+ names: { sdk: 'payment_release', mcp: 'ziggs_payment_release' },
244
+ descriptions: {
245
+ sdk: "Settle or refund an escrow hold. Use action='complete' to transfer held funds to toWalletId (work done), or action='refund' to return funds to the sender (work cancelled).",
246
+ mcp: "Settle or refund an escrow hold. action='complete' transfers held funds to toWalletId (work done); action='refund' returns funds to the sender (work cancelled).",
247
+ },
248
+ annotation: 'write',
249
+ params: {
250
+ holdId: {
251
+ type: 'string',
252
+ required: true,
253
+ description: 'Hold to settle (transactionId from the hold)',
254
+ },
255
+ action: {
256
+ type: 'string',
257
+ required: true,
258
+ enum: ['complete', 'refund'],
259
+ description: 'complete = pay out, refund = return',
260
+ },
261
+ toWalletId: { type: 'string', description: 'Destination wallet — required when action=complete' },
262
+ idempotencyKey: { type: 'string', description: 'Client-supplied retry-safety key' },
263
+ },
264
+ handler: async (args, env) => {
265
+ if (!args['holdId'])
266
+ throw new Error('holdId is required');
267
+ const action = args['action'];
268
+ if (action !== 'complete' && action !== 'refund')
269
+ throw new Error("action must be 'complete' or 'refund'");
270
+ if (action === 'complete' && !args['toWalletId'])
271
+ throw new Error('toWalletId is required when action=complete');
272
+ try {
273
+ const result = await client(env).release({
274
+ holdId: args['holdId'],
275
+ action,
276
+ toWalletId: args['toWalletId'],
277
+ idempotencyKey: args['idempotencyKey'],
278
+ });
279
+ return {
280
+ status: action === 'complete' ? 'settled' : 'refunded',
281
+ transactionId: result.transaction?.transactionId || null,
282
+ holdId: args['holdId'],
283
+ action,
284
+ };
285
+ }
286
+ catch (e) {
287
+ rethrowWithContext(e, 'Release failed');
288
+ }
289
+ },
290
+ };
291
+ export const paymentIssueGrantCapability = {
292
+ key: 'payment_issue_grant',
293
+ names: { sdk: 'payment_issue_grant', mcp: 'ziggs_payment_issue_grant' },
294
+ descriptions: {
295
+ sdk: "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.",
296
+ mcp: "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.",
297
+ },
298
+ annotation: 'write',
299
+ params: {
300
+ holderId: { type: 'string', required: true, description: 'Agent that will hold the grant' },
301
+ ...grantCaveatParams,
302
+ },
303
+ handler: async (args, env) => {
304
+ if (!args['holderId'])
305
+ throw new Error('holderId is required');
306
+ try {
307
+ const caveats = buildCaveats(args);
308
+ const { grant } = await client(env).issueGrant({
309
+ holderId: args['holderId'],
310
+ caveats,
311
+ });
312
+ return {
313
+ grantId: grant?.grantId || null,
314
+ holderId: grant?.holderId || args['holderId'],
315
+ caveats: grant?.caveats || caveats,
316
+ expiresAt: grant?.expiresAt || null,
317
+ };
318
+ }
319
+ catch (e) {
320
+ rethrowWithContext(e, 'Failed to issue payment grant');
321
+ }
322
+ },
323
+ };
324
+ export const paymentAttenuateGrantCapability = {
325
+ key: 'payment_attenuate_grant',
326
+ names: { sdk: 'payment_attenuate_grant', mcp: 'ziggs_payment_attenuate_grant' },
327
+ descriptions: {
328
+ sdk: '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.',
329
+ mcp: '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.',
330
+ },
331
+ annotation: 'write',
332
+ params: {
333
+ grantId: { type: 'string', required: true, description: 'Parent grant to attenuate' },
334
+ holderId: {
335
+ type: 'string',
336
+ required: true,
337
+ description: 'Agent that will hold the narrowed grant',
338
+ },
339
+ ...grantCaveatParams,
340
+ },
341
+ handler: async (args, env) => {
342
+ const grantId = args['grantId'];
343
+ if (!grantId)
344
+ throw new Error('grantId is required');
345
+ if (!args['holderId'])
346
+ throw new Error('holderId is required');
347
+ try {
348
+ const caveats = buildCaveats(args);
349
+ const { grant } = await client(env).attenuateGrant({
350
+ grantId,
351
+ holderId: args['holderId'],
352
+ caveats,
353
+ });
354
+ return {
355
+ grantId: grant?.grantId || null,
356
+ parentGrantId: grant?.parentGrantId || grantId,
357
+ holderId: grant?.holderId || args['holderId'],
358
+ caveats: grant?.caveats || caveats,
359
+ expiresAt: grant?.expiresAt || null,
360
+ };
361
+ }
362
+ catch (e) {
363
+ rethrowWithContext(e, 'Failed to attenuate payment grant');
364
+ }
365
+ },
366
+ };
367
+ export const paymentRevokeGrantCapability = {
368
+ key: 'payment_revoke_grant',
369
+ names: { sdk: 'payment_revoke_grant', mcp: 'ziggs_payment_revoke_grant' },
370
+ descriptions: {
371
+ sdk: 'Revoke a payment grant (and its attenuated children). The holder can no longer spend under it.',
372
+ mcp: 'Revoke a payment grant (and its attenuated children). The holder can no longer spend under it.',
373
+ },
374
+ annotation: 'destructive',
375
+ params: {
376
+ grantId: { type: 'string', required: true, description: 'Grant to revoke' },
377
+ },
378
+ handler: async (args, env) => {
379
+ const grantId = args['grantId'];
380
+ if (!grantId)
381
+ throw new Error('grantId is required');
382
+ try {
383
+ const result = await client(env).revokeGrant(grantId);
384
+ return { status: 'revoked', grantId, revoked: result?.revoked ?? null };
385
+ }
386
+ catch (e) {
387
+ rethrowWithContext(e, 'Failed to revoke payment grant');
388
+ }
389
+ },
390
+ };
391
+ // ZIG-893 — payment_list_grants stays retired; the wallet rail is part of the
392
+ // unified list_grants capability (scopeKind: 'wallet'). Grant *mutations* stay
393
+ // on the payment rail above.
394
+ export const PAYMENT_CAPABILITIES = [
395
+ paymentBalanceCapability,
396
+ paymentTransferCapability,
397
+ paymentWaitForApprovalCapability,
398
+ paymentHoldCapability,
399
+ paymentReleaseCapability,
400
+ paymentResolveWalletCapability,
401
+ paymentIssueGrantCapability,
402
+ paymentAttenuateGrantCapability,
403
+ paymentRevokeGrantCapability,
404
+ ];