@ziggs-ai/api-client 0.34.1 → 0.35.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.
@@ -165,7 +165,7 @@ export const agreementBuyCapability = {
165
165
  sdk: 'Ask a named counterparty to do work your side pays for. They provide, you pay. For work you would do for them instead, use agreement_bid; to reach whoever is available rather than someone named, use agreement_request.',
166
166
  mcp: 'Ask a named counterparty to do work your side pays for: they provide, you pay. Prefer claiming their listing first if they have one (ziggs_marketplace_view then ziggs_agreement_claim) — listings are take-it-or-leave-it and most published agents refuse direct proposals. Use this for bespoke terms, renegotiation, or a counterparty with no listing. For work you would do for them, use ziggs_agreement_bid; to reach whoever is available, use ziggs_agreement_request.',
167
167
  },
168
- annotation: 'write',
168
+ annotation: 'destructive',
169
169
  params: {
170
170
  counterparty: COUNTERPARTY_PARAM,
171
171
  chatId: CHAT_PARAM,
@@ -197,7 +197,7 @@ export const agreementBidCapability = {
197
197
  sdk: 'Offer to do work for a named counterparty, who pays. You provide. To publish the same offer to whoever wants it rather than one named party, use agreement_offer.',
198
198
  mcp: 'Offer to do work for a named counterparty, who pays: you provide. This is the direct form of a listing — to publish the same offer to whoever wants it, use ziggs_agreement_offer instead. To ask someone else to do work you pay for, use ziggs_agreement_buy.',
199
199
  },
200
- annotation: 'write',
200
+ annotation: 'destructive',
201
201
  params: {
202
202
  counterparty: COUNTERPARTY_PARAM,
203
203
  chatId: CHAT_PARAM,
@@ -229,7 +229,7 @@ export const agreementBrokerCapability = {
229
229
  sdk: 'Introduce a provider to a customer: the provider does the work, the counterparty pays, and neither side is you. The provider must already have a matching active offer.',
230
230
  mcp: 'Introduce a provider to a customer: the named provider does the work, the counterparty pays, and you are neither. The provider needs a matching active offer for this to stand. This is the one verb where naming a provider is the point — every other agreement verb infers it from the direction of work.',
231
231
  },
232
- annotation: 'write',
232
+ annotation: 'destructive',
233
233
  params: {
234
234
  counterparty: {
235
235
  ...COUNTERPARTY_PARAM,
@@ -268,7 +268,7 @@ export const agreementRequestCapability = {
268
268
  sdk: 'Post work you want done and will pay for, for whoever claims it. Whoever claims does the work. To offer work you would do instead, use agreement_offer.',
269
269
  mcp: 'Post work you want done and will pay for: whoever claims it does the work. Use this when nothing already listed fits, rather than proposing to named counterparties one at a time. To offer work you would do, use ziggs_agreement_offer. Claimable via ziggs_agreement_claim; visible in ziggs_marketplace_view.',
270
270
  },
271
- annotation: 'write',
271
+ annotation: 'destructive',
272
272
  params: {
273
273
  audience: AUDIENCE_PARAM,
274
274
  ...TERMS_PARAMS,
@@ -323,7 +323,7 @@ export const agreementOfferCapability = {
323
323
  sdk: 'Publish work you will do, for whoever claims it and pays. You provide. This is your listing: it stands until revoked or exhausted. To offer one named counterparty instead, use agreement_bid.',
324
324
  mcp: 'Publish work you will do, for whoever claims it and pays: you provide. This is your listing — it stands until revoked or exhausted, and claimers take it as posted rather than countering it. To offer one named counterparty instead, use ziggs_agreement_bid. To ask for work rather than supply it, use ziggs_agreement_request.',
325
325
  },
326
- annotation: 'write',
326
+ annotation: 'destructive',
327
327
  params: { audience: AUDIENCE_PARAM, ...TERMS_PARAMS, maxClaims: MAX_CLAIMS_PARAM },
328
328
  needsAgentId: true,
329
329
  handler: async (args, env) => {
@@ -366,7 +366,7 @@ export const listingPauseCapability = {
366
366
  sdk: 'Stop taking new customers on a listing you publish, without deleting it. New claims on it are refused until you resume it with listing_resume. Same listing, same id; hires already sold keep running. To end a listing for good, use agreement_revoke.',
367
367
  mcp: 'Stop taking new customers on a listing you publish, without deleting it. New claims on it are refused until you resume it with ziggs_listing_resume. Same listing, same id; hires already sold keep running. Use it when the person you act for asks you to stop taking work for now. To end a listing for good, use ziggs_agreement_revoke. Only the publisher of a listing can pause it.',
368
368
  },
369
- annotation: 'write',
369
+ annotation: 'destructive',
370
370
  params: { agreementId: LISTING_ID_PARAM },
371
371
  needsAgentId: true,
372
372
  handler: async (args, env) => {
@@ -382,7 +382,7 @@ export const listingResumeCapability = {
382
382
  sdk: 'Reopen a listing you paused with listing_pause: same listing, same id, taking new customers again. A listing closed because every seat is taken, or because its agent stopped answering, reopens on its own and needs no resume.',
383
383
  mcp: 'Reopen a listing you paused with ziggs_listing_pause: same listing, same id, taking new customers again. A listing closed because every seat is taken, or because its agent stopped answering, reopens on its own and needs no resume. Only the publisher of a listing can resume it.',
384
384
  },
385
- annotation: 'write',
385
+ annotation: 'destructive',
386
386
  params: { agreementId: LISTING_ID_PARAM },
387
387
  needsAgentId: true,
388
388
  handler: async (args, env) => {
@@ -399,7 +399,7 @@ export const agreementHandoffCapability = {
399
399
  sdk: 'Share an agent you have hired: the provider stays pinned, and whoever claims or approves becomes the customer the work is done for. Omit `to` to make it claimable by anyone. Handing off someone else\'s agent leaves that provider\'s approval pending — it must accept once before the hand-off can be claimed.',
400
400
  mcp: 'Share an agent you have hired: the provider stays pinned, and whoever claims or approves is the CUSTOMER the work is done for, never the worker. Omit `to` to publish it claimable; name `to` and that party approves directly. On a priced hand-off the claimer is also the payer; omit price (or 0) and nobody is billed for their tasks. Handing off someone else\'s agent leaves that provider\'s approval pending — it must accept once before the hand-off can be claimed. The provider is read from the parent hire, so you do not pass it.',
401
401
  },
402
- annotation: 'write',
402
+ annotation: 'destructive',
403
403
  params: {
404
404
  parentAgreementId: {
405
405
  type: 'string',
@@ -475,7 +475,7 @@ export const agreementSubcontractCapability = {
475
475
  sdk: 'Delegate a slice of an active parent agreement to another agent. Name the parent, the worker, and the slice. The worker must approve — never impersonated. A chat is optional: omit it and the slice lands in the parent agreement\'s own room.',
476
476
  mcp: 'Delegate a slice of an active parent agreement to another agent. Name the parent, the worker, and the slice. The worker must approve — never impersonated. A chat is optional: omit it and the slice lands in the parent agreement\'s own room. Spawn tasks for the worker under the sub-agreement once it is active.',
477
477
  },
478
- annotation: 'write',
478
+ annotation: 'destructive',
479
479
  params: {
480
480
  parentAgreementId: {
481
481
  type: 'string',
@@ -86,7 +86,7 @@ export const agreementClaimCapability = {
86
86
  sdk: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim and there are no negotiation turns. Claims a request (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party. It activates at once when you can consent for your own side — an agent claiming help for a job it is already doing should name that job with mandateAgreementId; without it the formation waits on your human before anything can be spawned under it. A hand-off is claimable only after its provider has accepted (409 until then). Find requests/offers with marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with agreement_respond instead, not claimed. To hire an agent into a room, claim its listing with that room\'s chatId.',
87
87
  mcp: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim and there are no negotiation turns. Claims a request (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party. It activates at once when you can consent for your own side — an agent claiming help for a job it is already doing should name that job with mandateAgreementId; without it the formation waits on your human before anything can be spawned under it. A hand-off is claimable only after its provider has accepted (409 until then). Find requests/offers with ziggs_marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with ziggs_agreement_respond instead, not claimed. You cannot claim your own broadcast. To hire an agent into a room, claim its listing with that room\'s chatId.',
88
88
  },
89
- annotation: 'write',
89
+ annotation: 'destructive',
90
90
  params: {
91
91
  agreementId: {
92
92
  type: 'string',
@@ -107,12 +107,30 @@ export const agreementClaimCapability = {
107
107
  type: 'string',
108
108
  description: 'The room this hire is for — only when claiming an agent\'s listing. When the hire activates the agent joins that exact room, with no invitation and no second approval; if it waits for a yes, the room shows it as a proposal. You must be in the room. The answer\'s room.status says where it stands.',
109
109
  },
110
+ connections: {
111
+ type: 'object',
112
+ description: 'Only when the listing\'s requiredConnections name a kind (e.g. ledger, customer): one of YOUR connections for each, as { "<kind>": "<connectionId>" }. The hire is granted exactly that connection and the grant ends with it. Leave it out when claiming inside a job (mandateAgreementId) — the job\'s choice is used.',
113
+ },
110
114
  },
111
115
  needsAgentId: true,
112
116
  handler: async (args, env) => {
113
117
  const declaredMandate = args['mandateAgreementId'];
114
- const { agreement, kind, room } = await claimOpenAgreement(args['agreementId'], fullCreds(env), { ...(typeof declaredMandate === 'string' && declaredMandate ? { mandateAgreementId: declaredMandate } : {}), ...(typeof args['parentAgreementId'] === 'string' ? { parentAgreementId: args['parentAgreementId'] } : {}), ...(typeof args['chatId'] === 'string' && args['chatId'] ? { chatId: args['chatId'] } : {}) });
118
+ const { agreement, kind, room } = await claimOpenAgreement(args['agreementId'], fullCreds(env), { ...(typeof declaredMandate === 'string' && declaredMandate ? { mandateAgreementId: declaredMandate } : {}), ...(typeof args['parentAgreementId'] === 'string' ? { parentAgreementId: args['parentAgreementId'] } : {}), ...(typeof args['chatId'] === 'string' && args['chatId'] ? { chatId: args['chatId'] } : {}), ...(claimConnections(args['connections']) ?? {}) });
115
119
  return presentClaimResult(agreement, kind, env, room);
116
120
  },
117
121
  };
122
+ /** `{ connections }` when the caller named any, as strings; anything else is refused here. */
123
+ function claimConnections(raw) {
124
+ if (raw === undefined || raw === null)
125
+ return undefined;
126
+ if (typeof raw !== 'object' || Array.isArray(raw))
127
+ throw new Error('connections must be an object of { <kind>: <connectionId> }');
128
+ const connections = {};
129
+ for (const [kind, id] of Object.entries(raw)) {
130
+ if (typeof id !== 'string' || !id.trim())
131
+ throw new Error(`connections.${kind} must be one of your connection ids`);
132
+ connections[kind] = id.trim();
133
+ }
134
+ return Object.keys(connections).length ? { connections } : undefined;
135
+ }
118
136
  export const AGREEMENT_CAPABILITIES = [agreementClaimCapability];
@@ -479,7 +479,7 @@ export const shareArtifactCapability = {
479
479
  'in the chat you are working in. The receiver then reads it with ziggs_context_read ' +
480
480
  'via=artifact:<id>.',
481
481
  },
482
- annotation: 'write',
482
+ annotation: 'destructive',
483
483
  params: {
484
484
  artifactId: {
485
485
  type: 'string',
@@ -555,7 +555,7 @@ export const attachArtifactCapability = {
555
555
  'to the container. To hand it to ONE specific agent without opening a chat, use ' +
556
556
  'ziggs_artifact_share instead.',
557
557
  },
558
- annotation: 'write',
558
+ annotation: 'destructive',
559
559
  params: {
560
560
  artifactId: { type: 'string', required: true, description: 'Artifact to attach' },
561
561
  chatId: { type: 'string', description: 'Chat to attach it to' },
@@ -797,7 +797,7 @@ export const reextractArtifactCapability = {
797
797
  mcp: 'Re-queue extract for a file you authored (extractionStatus ok or failed only). ' +
798
798
  'Creates a new derived text artifact and supersedes the old one — no in-place mutate.',
799
799
  },
800
- annotation: 'write',
800
+ annotation: 'destructive',
801
801
  params: {
802
802
  artifactId: { type: 'string', required: true, description: 'File artifact id' },
803
803
  },
@@ -25,7 +25,7 @@ export const connectionProxyCapability = {
25
25
  'Not for remote MCP servers (provider "mcp") — those use ziggs_mcp_tool_call / ziggs_mcp_tools_list; proxy refuses them with "Unknown provider: mcp". ' +
26
26
  "Don't know connectionId/grantId yet? Call ziggs_connection_list first.",
27
27
  },
28
- annotation: "write",
28
+ annotation: "destructive",
29
29
  params: {
30
30
  connectionId: {
31
31
  type: "string",
@@ -83,7 +83,7 @@ export const requestConnectionCapability = {
83
83
  "Opens a connection-consent card in the chat you pass; the answer says who got it and where they approve it (there is no MCP tool to approve it). " +
84
84
  "On approval the server is connected (browser OAuth if needed) and you are granted the tools; the result shows up in ziggs_connection_list for use with ziggs_mcp_tools_list / ziggs_mcp_tool_call.",
85
85
  },
86
- annotation: "write",
86
+ annotation: "destructive",
87
87
  params: {
88
88
  chatId: {
89
89
  type: "string",
@@ -375,7 +375,7 @@ export const contextRequestCapability = {
375
375
  sdk: 'Ask a human owner for temporary read-only access to one artifact you were given a reference for. Use your existing chat with that owner. Creates an approval agreement; gives no access until the owner approves. You are the only recipient. After approval, open the original artifact. Never use a generic service agreement as a substitute for this access request.',
376
376
  mcp: 'Request access to a restricted artifact directly from its human owner. Pass the artifact id you were given, your conversation with the owner, and in ownerId who to ask (a pick the server cannot use here is refused with the list). This creates a pending approval agreement, NOT a grant. No prior grant is needed. The human approves at the agreement link. After approval, ziggs_open the original artifact. Scope is read-only, one artifact, for 1–168 hours (default 24); no other artifacts or chats. Include the intended summary recipient in reason. Keep polling for the decision when asked to wait. A request does not confirm that the artifact exists or belongs to the named owner; ownership is checked at approval.',
377
377
  },
378
- annotation: 'write', needsAgentId: true,
378
+ annotation: 'destructive', needsAgentId: true,
379
379
  params: {
380
380
  artifactId: { type: 'string', required: true },
381
381
  chatId: { type: 'string', required: true },
@@ -411,7 +411,7 @@ export const accessRequestCapability = {
411
411
  key: 'access_request',
412
412
  names: { sdk: 'access_request', mcp: 'ziggs_access_request' },
413
413
  title: 'Request access for yourself',
414
- annotation: 'write', needsAgentId: true,
414
+ annotation: 'destructive', needsAgentId: true,
415
415
  descriptions: {
416
416
  sdk: 'Request optional bounded access to one artifact, chat, agreement context or task. Creates only a pending approval; never grants access or approves. Name who to ask in recipientId: your work brief offers them for this wake — workBrief.actingFor.customer.id (the customer of the hire you are working in, for their own accounts and resources) or workBrief.actingFor.operator.id (your operator, for tools you bring). Anyone else is refused, and the refusal lists who you can ask. Artifact requests also need chatId, the conversation they approve in. To create work under an agreement you can read but not work on (a task create refused with WORK_ACCESS_REQUIRED), request permission=work on it; only someone who can work on it may approve, and it lasts until the agreement ends. Use scopeId for a known artifact; or scopeKind=artifact with agreementId of your active hire to request its system-maintained workspace summary (workspace name/type and agents, including future updates, not private conversations or document bodies). Send the returned appUrl, wait for human approval, then open the returned scope. Hiring alone grants no source access. Revocation and expiry are checked by the system.',
417
417
  mcp: 'Request access for this agent, never approve or grant it. Use scopeId for an artifact, chat, agreement or task; scopeKind=artifact with agreementId of your active hire resolves its system-maintained workspace summary artifact. Name who to ask in recipientId; a pick the server cannot use here is refused, and the refusal lists who you can ask. Artifact requests also require chatId. permission=work on an agreement asks to create work under it; send it when a refusal says you can read but not work on it. Explain the scope and lifetime; summary access includes future updates only to that artifact. Surface appUrl and wait for human approval; then open the returned scope. Pending grants no access.',
@@ -7,15 +7,12 @@
7
7
  * the inbox, and the SDK has no context-grant revoke.
8
8
  *
9
9
  * Everything else stays callable. It is named by the catalog and reached
10
- * through the read or write dispatcher, not mounted as its own schema.
10
+ * through the read or write dispatcher, not mounted as its own schema. A
11
+ * tool's destructive hint never decides this list: the write dispatcher is
12
+ * itself marked destructive, so a host can warn before any dispatched write,
13
+ * and nothing has to be native just to carry the hint.
11
14
  */
12
15
  export declare const EVERYDAY_MCP_TOOLS: readonly ["ziggs_inbox", "ziggs_inbox_peek", "ziggs_inbox_ack", "ziggs_open", "ziggs_context_read", "ziggs_chat_open", "ziggs_chat_send", "ziggs_task_set_result", "ziggs_agreement_revoke", "ziggs_context_revoke_grant"];
13
- /**
14
- * Irreversible tools stay natively listed, and the write dispatcher refuses
15
- * them. A host reads `destructiveHint` off the tool it is asked to run; a
16
- * dispatcher would present the warning as an ordinary write.
17
- */
18
- export declare const EVERYDAY_DESTRUCTIVE_MCP: readonly ["ziggs_agreement_revoke", "ziggs_context_revoke_grant"];
19
16
  /** Everyday MCP tools the hosted brain does not register. */
20
17
  export declare const EVERYDAY_NO_SDK_TWIN: readonly ["ziggs_inbox", "ziggs_inbox_peek", "ziggs_inbox_ack", "ziggs_context_revoke_grant"];
21
18
  /** SDK names of the everyday set. Same order as the MCP list, twins only. */
@@ -7,7 +7,10 @@
7
7
  * the inbox, and the SDK has no context-grant revoke.
8
8
  *
9
9
  * Everything else stays callable. It is named by the catalog and reached
10
- * through the read or write dispatcher, not mounted as its own schema.
10
+ * through the read or write dispatcher, not mounted as its own schema. A
11
+ * tool's destructive hint never decides this list: the write dispatcher is
12
+ * itself marked destructive, so a host can warn before any dispatched write,
13
+ * and nothing has to be native just to carry the hint.
11
14
  */
12
15
  export const EVERYDAY_MCP_TOOLS = [
13
16
  'ziggs_inbox',
@@ -23,16 +26,8 @@ export const EVERYDAY_MCP_TOOLS = [
23
26
  'ziggs_chat_open',
24
27
  'ziggs_chat_send',
25
28
  'ziggs_task_set_result',
26
- // And every irreversible tool — see EVERYDAY_DESTRUCTIVE_MCP below.
27
- 'ziggs_agreement_revoke',
28
- 'ziggs_context_revoke_grant',
29
- ];
30
- /**
31
- * Irreversible tools stay natively listed, and the write dispatcher refuses
32
- * them. A host reads `destructiveHint` off the tool it is asked to run; a
33
- * dispatcher would present the warning as an ordinary write.
34
- */
35
- export const EVERYDAY_DESTRUCTIVE_MCP = [
29
+ // The two ways out, everyday because taking access back must never need a
30
+ // lookup first (Ido, 2026-09-16).
36
31
  'ziggs_agreement_revoke',
37
32
  'ziggs_context_revoke_grant',
38
33
  ];
@@ -16,6 +16,6 @@ export { CONNECTION_CAPABILITIES, connectionProxyCapability, requestConnectionCa
16
16
  export { DISCOVERY_CAPABILITIES, agentSearchCapability, agentGetCapability, } from './discovery.js';
17
17
  export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, findArtifactsCapability, type UnsearchedContext, shareArtifactCapability, attachArtifactCapability, uploadArtifactUrlCapability, completeArtifactFileCapability, downloadArtifactCapability, reextractArtifactCapability, } from './artifacts.js';
18
18
  export { CHAT_CAPABILITIES, openConversationCapability, presentSendResult, type SendPresentation, } from './chat.js';
19
- export { EVERYDAY_MCP_TOOLS, EVERYDAY_DESTRUCTIVE_MCP, EVERYDAY_NO_SDK_TWIN, EVERYDAY_SDK_TOOLS, } from './everyday.js';
19
+ export { EVERYDAY_MCP_TOOLS, EVERYDAY_NO_SDK_TWIN, EVERYDAY_SDK_TOOLS, } from './everyday.js';
20
20
  export { annotationForSdkTool } from './toolLane.js';
21
21
  export { presentResourceActions } from './resourceActions.js';
@@ -16,6 +16,6 @@ export { CONNECTION_CAPABILITIES, connectionProxyCapability, requestConnectionCa
16
16
  export { DISCOVERY_CAPABILITIES, agentSearchCapability, agentGetCapability, } from './discovery.js';
17
17
  export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, findArtifactsCapability, shareArtifactCapability, attachArtifactCapability, uploadArtifactUrlCapability, completeArtifactFileCapability, downloadArtifactCapability, reextractArtifactCapability, } from './artifacts.js';
18
18
  export { CHAT_CAPABILITIES, openConversationCapability, presentSendResult, } from './chat.js';
19
- export { EVERYDAY_MCP_TOOLS, EVERYDAY_DESTRUCTIVE_MCP, EVERYDAY_NO_SDK_TWIN, EVERYDAY_SDK_TOOLS, } from './everyday.js';
19
+ export { EVERYDAY_MCP_TOOLS, EVERYDAY_NO_SDK_TWIN, EVERYDAY_SDK_TOOLS, } from './everyday.js';
20
20
  export { annotationForSdkTool } from './toolLane.js';
21
21
  export { presentResourceActions } from './resourceActions.js';
@@ -63,7 +63,7 @@ export const redeemIntroductionCapability = {
63
63
  sdk: `Redeem an introduction token someone handed you (POST /introductions/:token/redeem). ${INBOUND_APPROVAL} If they are a stranger, this stages a link proposal from them to you which both humans must approve. After approval, reach their agent with chat_open using the returned counterparty.agentId. If you already share an org or a link, it says so instead of proposing another. One use — a second redeem is refused.`,
64
64
  mcp: `Redeem an introduction token someone handed you (POST /introductions/:token/redeem). ${INBOUND_APPROVAL} If they are a stranger, this stages a link proposal from them to you which both humans must approve. After approval, open a room with ziggs_chat_open using the returned counterparty.agentId. If you already share an org or a link, it says so instead of proposing another. One use — a second redeem is refused. A self-chosen display name is not verified identity.`,
65
65
  },
66
- annotation: 'write',
66
+ annotation: 'destructive',
67
67
  params: {
68
68
  token: {
69
69
  type: 'string',
@@ -142,7 +142,7 @@ export const revokeIntroductionCapability = {
142
142
  // A write, not a destructive act: nobody holds anything from an unredeemed
143
143
  // hello, so taking one back removes no access and loses no data. The
144
144
  // destructive annotation is reserved for the two tools that end live access.
145
- annotation: 'write',
145
+ annotation: 'destructive',
146
146
  params: {
147
147
  token: {
148
148
  type: 'string',
@@ -140,7 +140,7 @@ export const proposeLinkCapability = {
140
140
  sdk: "Connect with a person: pass their email, or the id of an agent that answers to them, and Ziggs delivers the invitation. The link is with the PERSON either way — an agent id is only a way to find its owner, and an agent that answers to an organisation is refused, because a link connects two people. Leave `to` out to get a share link you hand over yourself. A link is reach and nothing else: no chat, no money, no work. The answer is the same every time, so it never tells you whether that address has an account.",
141
141
  mcp: "Connect with a person (NOT a third-party service connection — see ziggs_connection_list for that): pass their email, or the id of an agent that answers to them, and Ziggs delivers the invitation. The link is with the PERSON either way — an agent id is only a way to find its owner, and an agent answering to an organisation is refused, because a link connects two people. Leave `to` out and you get shareUrl: one public page that is the whole invite, which you hand to your human to paste wherever they like. Someone with no Ziggs account signs up from that page and accepts in the same step, and their assistant handed the same URL reads the connect instructions off it. A link is reach and nothing else: no chat, no money, no work. The answer is identical every time, so it never reveals whether an address has an account. Approve links proposed to you with ziggs_agreement_respond; end one with ziggs_agreement_revoke.",
142
142
  },
143
- annotation: 'write',
143
+ annotation: 'destructive',
144
144
  params: {
145
145
  to: {
146
146
  type: 'string',
@@ -40,8 +40,8 @@ export const paymentBalanceCapability = {
40
40
  names: { sdk: 'payment_balance', mcp: 'ziggs_payment_balance' },
41
41
  title: 'Check your points',
42
42
  descriptions: {
43
- sdk: "Check the caller's points balance and available balance (total minus points held against active agreements). Points move as a consequence of an agreement settling.",
44
- mcp: "Check the caller's points balance and available balance (total minus points held against active agreements). Points move as a consequence of an agreement settling — there is no tool to move them on their own, so earning and spending both happen when an agreement settles.",
43
+ sdk: "Check the caller's points balance and available balance (total minus points held against active agreements). Points settle through work. They cannot be cashed out or resold; point purchases are only available in the Ziggs app.",
44
+ mcp: "Check the caller's points balance and available balance (total minus points held against active agreements). Points settle through work. They cannot be cashed out or resold, and cannot be transferred on their own. Point purchases are only available in the Ziggs app; this connector has no purchase or checkout tool.",
45
45
  },
46
46
  annotation: 'read-only',
47
47
  params: {},
@@ -76,7 +76,7 @@ export const cancelTaskCapability = {
76
76
  sdk: 'Cancel one task (PATCH /tasks/:id/cancel). A held graph withdraws by cancelling its root — that is one act, not a blocked state. Cancel ends this task; it does not amend a live hire.',
77
77
  mcp: 'Cancel one task (PATCH /tasks/:id/cancel). A held graph withdraws by cancelling its root — that is one act, not a blocked state. Cancel ends this task; it does not amend a live hire.',
78
78
  },
79
- annotation: 'write',
79
+ annotation: 'destructive',
80
80
  params: {
81
81
  taskId: {
82
82
  type: 'string',
@@ -325,6 +325,12 @@ export interface ClaimOptions {
325
325
  * agent joins that room; the claimer must take part in it.
326
326
  */
327
327
  chatId?: string;
328
+ /**
329
+ * Your connection for each kind of access the listing asks for,
330
+ * `{ <kind>: <connectionId> }`. The hire is granted exactly that connection. A claim made inside a job (mandateAgreementId)
331
+ * takes the job's and names none.
332
+ */
333
+ connections?: Record<string, string>;
328
334
  }
329
335
  /**
330
336
  * Where a hire made in a room stands on getting its agent in.
@@ -554,7 +554,7 @@ export async function claimAgreement(agreementId, creds, opts = {}) {
554
554
  const res = await fetch(`${getAgreementBaseUrl()}/${encodeURIComponent(agreementId)}/claim`, {
555
555
  method: 'POST',
556
556
  headers: buildHeaders(creds),
557
- body: JSON.stringify({ ...(opts.mandateAgreementId ? { mandateAgreementId: opts.mandateAgreementId } : {}), ...(opts.parentAgreementId ? { parentAgreementId: opts.parentAgreementId } : {}), ...(opts.chatId ? { chatId: opts.chatId } : {}) }),
557
+ body: JSON.stringify({ ...(opts.mandateAgreementId ? { mandateAgreementId: opts.mandateAgreementId } : {}), ...(opts.parentAgreementId ? { parentAgreementId: opts.parentAgreementId } : {}), ...(opts.chatId ? { chatId: opts.chatId } : {}), ...(opts.connections && Object.keys(opts.connections).length ? { connections: opts.connections } : {}) }),
558
558
  });
559
559
  if (!res.ok) {
560
560
  const body = await res.text().catch(() => '');
@@ -15,13 +15,13 @@ export declare class AssistantConnectionClient {
15
15
  private readonly baseUrl;
16
16
  constructor(creds: Creds, baseUrl?: string);
17
17
  request<T>(path: string, body?: unknown): Promise<T>;
18
- /** Live: a workspace joined since connecting shows up here with its assistant. */
18
+ /** Read existing enabled assistant hires. Provisioning is an explicit sync action. */
19
19
  workspaces(): Promise<AssistantWorkspace[]>;
20
20
  /** Which of the connection's assistants is on an existing thing. */
21
21
  resolve(kind: string, id: string): Promise<{
22
22
  workspace: AssistantWorkspace | null;
23
23
  choices: AssistantWorkspace[];
24
24
  }>;
25
- inbox(waitSeconds?: number): Promise<Record<string, unknown>>;
25
+ inbox(waitSeconds?: number, snapshot?: boolean): Promise<Record<string, unknown>>;
26
26
  ack(upTo: string, handledResourceIds?: string[]): Promise<unknown>;
27
27
  }
@@ -31,7 +31,7 @@ export class AssistantConnectionClient {
31
31
  }
32
32
  return JSON.parse(text);
33
33
  }
34
- /** Live: a workspace joined since connecting shows up here with its assistant. */
34
+ /** Read existing enabled assistant hires. Provisioning is an explicit sync action. */
35
35
  async workspaces() {
36
36
  return (await this.request('workspaces'))
37
37
  .workspaces;
@@ -40,8 +40,8 @@ export class AssistantConnectionClient {
40
40
  resolve(kind, id) {
41
41
  return this.request(`resolve?${new URLSearchParams({ kind, id })}`);
42
42
  }
43
- inbox(waitSeconds = 0) {
44
- return this.request(`inbox?wait=${waitSeconds}`);
43
+ inbox(waitSeconds = 0, snapshot = false) {
44
+ return this.request(`inbox${snapshot ? "/snapshot" : ""}?wait=${waitSeconds}`);
45
45
  }
46
46
  ack(upTo, handledResourceIds = []) {
47
47
  return this.request('inbox/ack', { upTo, handledResourceIds });
@@ -20,6 +20,10 @@ export declare class InboxClient {
20
20
  constructor(operatorKey: string, agentId?: string, baseUrl?: string, instanceId?: string);
21
21
  private headers;
22
22
  private inboxUrl;
23
+ /** Read without acquiring worker ownership or recording receipts. */
24
+ getSnapshot(opts?: InboxReadOptions & {
25
+ signal?: AbortSignal;
26
+ }): Promise<InboxEnvelope>;
23
27
  getInbox(opts?: InboxReadOptions & {
24
28
  signal?: AbortSignal;
25
29
  }): Promise<InboxEnvelope>;
@@ -47,6 +47,16 @@ export class InboxClient {
47
47
  }
48
48
  return url.toString();
49
49
  }
50
+ /** Read without acquiring worker ownership or recording receipts. */
51
+ async getSnapshot(opts = {}) {
52
+ const res = await fetch(this.inboxUrl('/inbox/snapshot', opts), {
53
+ method: 'GET', headers: this.headers(), signal: opts.signal,
54
+ });
55
+ const body = await res.text().catch(() => '');
56
+ if (!res.ok)
57
+ throw pollSurfaceError('InboxClient.getSnapshot', res, body);
58
+ return JSON.parse(body);
59
+ }
50
60
  async getInbox(opts = {}) {
51
61
  const res = await fetch(this.inboxUrl('/inbox', opts), {
52
62
  headers: this.headers(),
@@ -10,10 +10,6 @@ export interface WalletBalance {
10
10
  balance: number;
11
11
  availableBalance: number;
12
12
  }
13
- export interface PaymentTransactionView {
14
- transactionId?: string;
15
- [key: string]: unknown;
16
- }
17
13
  /**
18
14
  * A resolved wallet reference (GET /payments/wallets/resolve).
19
15
  *
@@ -24,27 +20,6 @@ export interface PaymentTransactionView {
24
20
  */
25
21
  import type { WalletRef } from '@ziggs-ai/contracts';
26
22
  export type { WalletRef };
27
- export type TransferResult = {
28
- status: 'approval_required';
29
- approvalId: string | null;
30
- expiresAt: string | null;
31
- reason: string | null;
32
- toWalletId: string;
33
- amount: number;
34
- } | {
35
- status: 'transferred';
36
- transactionId: string | null;
37
- toWalletId: string;
38
- amount: number;
39
- };
40
- export interface HoldResult {
41
- transaction?: PaymentTransactionView;
42
- [key: string]: unknown;
43
- }
44
- export interface ReleaseResult {
45
- transaction?: PaymentTransactionView;
46
- [key: string]: unknown;
47
- }
48
23
  /**
49
24
  * A payment grant as this rail answers it.
50
25
  *
@@ -69,40 +44,14 @@ export interface RevokeGrantResult {
69
44
  revoked?: unknown;
70
45
  [key: string]: unknown;
71
46
  }
72
- export interface PaymentApproval {
73
- approvalId?: string;
74
- status?: string;
75
- executedTransactionId?: string | null;
76
- [key: string]: unknown;
77
- }
78
- export type WaitForApprovalResult = {
79
- status: 'gone';
80
- } | {
81
- status: 'executed';
82
- approval: PaymentApproval;
83
- transactionId: string | null;
84
- } | {
85
- status: 'rejected' | 'expired' | 'timeout';
86
- approval: PaymentApproval;
87
- };
88
- /**
89
- * Who to pay on a transfer. Kind is declared by the caller — never inferred
90
- * from id shape. A bare string is only a wallet id (`wal_…`).
91
- */
92
- export type TransferPayee = string | {
93
- walletId: string;
94
- } | {
95
- agentId: string;
96
- } | {
97
- userId: string;
98
- };
99
47
  /**
100
48
  * the one payments client for every surface (agent-sdk, ziggs-mcp,
101
49
  * scripts). Consolidates the former agent-sdk private ZiggsPayClient. Wallet
102
50
  * ops ride the operator key; agent-impersonated calls send X-Agent-Id and are
103
- * policy-gated server-side (transfers above threshold return
104
- * `approval_required`). Wallet-rail grant mutations (issue / attenuate /
105
- * revoke) live here; grant *listing* is the unified GET /grants.
51
+ * policy-gated server-side. Points move only when an agreement or a task
52
+ * settles, and the server does that; there is no standalone movement verb on
53
+ * this client. Wallet-rail grant mutations (issue / attenuate / revoke) live
54
+ * here; grant *listing* is the unified GET /grants.
106
55
  */
107
56
  export declare class PaymentsClient {
108
57
  private readonly operatorKey;
@@ -128,60 +77,6 @@ export declare class PaymentsClient {
128
77
  userId?: string;
129
78
  agentId?: string;
130
79
  }): Promise<WalletRef | null>;
131
- /**
132
- * Resolve `transfer.to` to a wallet id. String form is wallet-only; agent /
133
- * user payees must name their kind.
134
- */
135
- private resolvePayeeWalletId;
136
- transfer({ to, amount, idempotencyKey, description, paymentGrantId, agreementId, }: {
137
- to: TransferPayee;
138
- amount: number;
139
- idempotencyKey?: string;
140
- description?: string;
141
- paymentGrantId?: string;
142
- /**
143
- * The hire this spend belongs to. Used for attribution, and to prefer that
144
- * hire's own grant over a standing one — a preference about which budget is
145
- * billed, never about which grants may be spent. That answer is the
146
- * server's, and it turns on who gave the grant.
147
- */
148
- agreementId?: string;
149
- }): Promise<TransferResult>;
150
- hold({ amount, idempotencyKey, description, paymentGrantId, agreementId, }: {
151
- amount: number;
152
- idempotencyKey?: string;
153
- description?: string;
154
- paymentGrantId?: string;
155
- /**
156
- * The hire this spend belongs to. Used for attribution, and to prefer that
157
- * hire's own grant over a standing one — a preference about which budget is
158
- * billed, never about which grants may be spent. That answer is the
159
- * server's, and it turns on who gave the grant.
160
- */
161
- agreementId?: string;
162
- }): Promise<HoldResult>;
163
- /**
164
- * Choose the grant that actually covers this spend.
165
- *
166
- * This used to be `grants[0]` — the first active grant, matched against
167
- * nothing. An agent holding both its standing grant and an
168
- * agreement-conferred one presented whichever came back first, the backend
169
- * correctly refused it against wallet or amount, and the agent was refused a
170
- * spend it was genuinely authorized to make.
171
- *
172
- * The source wallet is read rather than assumed: a grant on some other
173
- * wallet cannot authorize money leaving this one, and the agent may hold
174
- * grants on a counterparty's wallet from an engagement. A failure to read it
175
- * is not fatal — selection then skips the wallet test and the server still
176
- * validates, which is the same position we were in before.
177
- */
178
- private selectPaymentGrant;
179
- release({ holdId, action, toWalletId, idempotencyKey, }: {
180
- holdId: string;
181
- action?: string;
182
- toWalletId?: string;
183
- idempotencyKey?: string;
184
- }): Promise<ReleaseResult>;
185
80
  history(params?: {
186
81
  limit?: number;
187
82
  offset?: number;
@@ -213,16 +108,6 @@ export declare class PaymentsClient {
213
108
  description?: string;
214
109
  idempotencyKey?: string;
215
110
  }): Promise<unknown>;
216
- approvals({ status }?: {
217
- status?: string;
218
- }): Promise<PaymentApproval[]>;
219
- getApproval(approvalId: string): Promise<PaymentApproval | null>;
220
- decide(approvalId: string, decision: string, note?: string): Promise<unknown>;
221
- waitForApproval(approvalId: string, opts?: {
222
- pollMs?: number;
223
- timeoutMs?: number;
224
- signal?: AbortSignal;
225
- }): Promise<WaitForApprovalResult>;
226
111
  private _request;
227
112
  private _get;
228
113
  private _post;
@@ -1,7 +1,6 @@
1
1
  import { getBackendUrl } from '../utils/urlUtils.js';
2
2
  import { buildOperatorHeaders } from './operatorHeaders.js';
3
3
  import { GrantsClient } from './GrantsClient.js';
4
- import { describeNoGrant, selectPaymentGrant, } from './paymentGrantSelection.js';
5
4
  import { throwApiError } from '../shared/apiError.js';
6
5
  function randomIdempotencyKey(prefix = 'op') {
7
6
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
@@ -10,9 +9,10 @@ function randomIdempotencyKey(prefix = 'op') {
10
9
  * the one payments client for every surface (agent-sdk, ziggs-mcp,
11
10
  * scripts). Consolidates the former agent-sdk private ZiggsPayClient. Wallet
12
11
  * ops ride the operator key; agent-impersonated calls send X-Agent-Id and are
13
- * policy-gated server-side (transfers above threshold return
14
- * `approval_required`). Wallet-rail grant mutations (issue / attenuate /
15
- * revoke) live here; grant *listing* is the unified GET /grants.
12
+ * policy-gated server-side. Points move only when an agreement or a task
13
+ * settles, and the server does that; there is no standalone movement verb on
14
+ * this client. Wallet-rail grant mutations (issue / attenuate / revoke) live
15
+ * here; grant *listing* is the unified GET /grants.
16
16
  */
17
17
  export class PaymentsClient {
18
18
  operatorKey;
@@ -61,139 +61,6 @@ export class PaymentsClient {
61
61
  const res = (await this._get(`/payments/wallets/resolve?${params}`));
62
62
  return res['wallet'] || null;
63
63
  }
64
- /**
65
- * Resolve `transfer.to` to a wallet id. String form is wallet-only; agent /
66
- * user payees must name their kind.
67
- */
68
- async resolvePayeeWalletId(to) {
69
- if (typeof to === 'string') {
70
- const id = to.trim();
71
- if (!id)
72
- throw new Error('transfer: `to` is required');
73
- if (id.startsWith('wal_'))
74
- return id;
75
- throw new Error('transfer: string `to` must be a wallet id (wal_…); pass { agentId } or { userId } for a principal');
76
- }
77
- if ('walletId' in to && typeof to.walletId === 'string' && to.walletId.trim()) {
78
- return to.walletId.trim();
79
- }
80
- if ('agentId' in to && typeof to.agentId === 'string' && to.agentId.trim()) {
81
- const w = await this.resolve({ agentId: to.agentId.trim() });
82
- if (!w?.walletId) {
83
- throw new Error(`transfer: could not resolve wallet for agent "${to.agentId}"`);
84
- }
85
- return w.walletId;
86
- }
87
- if ('userId' in to && typeof to.userId === 'string' && to.userId.trim()) {
88
- const w = await this.resolve({ userId: to.userId.trim() });
89
- if (!w?.walletId) {
90
- throw new Error(`transfer: could not resolve wallet for user "${to.userId}"`);
91
- }
92
- return w.walletId;
93
- }
94
- throw new Error('transfer: `to` must be a wallet id (wal_…) or { walletId | agentId | userId }');
95
- }
96
- async transfer({ to, amount, idempotencyKey, description, paymentGrantId, agreementId, }) {
97
- if (to == null || to === '')
98
- throw new Error('transfer: `to` is required');
99
- if (!(Number.isInteger(amount) && amount > 0))
100
- throw new Error('transfer: `amount` must be a positive integer (cents)');
101
- // The recipient is resolved BEFORE choosing a grant: an `allowed_recipients`
102
- // caveat cannot be checked against a name the client has not resolved yet.
103
- const toWalletId = await this.resolvePayeeWalletId(to);
104
- // Choose the grant that covers this spend when the caller omitted one.
105
- // An explicit id still wins — a caller who names a grant means it.
106
- if (this.agentId && !paymentGrantId) {
107
- const picked = await this.selectPaymentGrant({ amount, toWalletId, agreementId });
108
- if (!picked.grant) {
109
- throw new Error(describeNoGrant('transfer', { amount, toWalletId, agreementId }, picked.rejected));
110
- }
111
- paymentGrantId = picked.grant.grantId;
112
- }
113
- const result = (await this._post('/payments/transfer', {
114
- toWalletId,
115
- amount,
116
- idempotencyKey: idempotencyKey || randomIdempotencyKey('xfer'),
117
- description,
118
- paymentGrantId,
119
- }));
120
- if (result?.['status'] === 'approval_required') {
121
- const approval = result['approval'] || {};
122
- return {
123
- status: 'approval_required',
124
- approvalId: approval['approvalId'] || null,
125
- expiresAt: approval['expiresAt'] || null,
126
- reason: approval['reason'] || 'Amount exceeds auto-approve threshold',
127
- toWalletId,
128
- amount,
129
- };
130
- }
131
- const tx = result?.['transaction'];
132
- return {
133
- status: 'transferred',
134
- transactionId: tx?.['transactionId'] || null,
135
- toWalletId,
136
- amount,
137
- };
138
- }
139
- async hold({ amount, idempotencyKey, description, paymentGrantId, agreementId, }) {
140
- if (!(Number.isInteger(amount) && amount > 0))
141
- throw new Error('hold: `amount` must be a positive integer (cents)');
142
- // A hold reserves the owner's money, so it needs a grant like a transfer.
143
- // It has no counterparty, so no recipient is offered to the caveats.
144
- if (this.agentId && !paymentGrantId) {
145
- const picked = await this.selectPaymentGrant({ amount, agreementId });
146
- if (!picked.grant) {
147
- throw new Error(describeNoGrant('hold', { amount, agreementId }, picked.rejected));
148
- }
149
- paymentGrantId = picked.grant.grantId;
150
- }
151
- return (await this._post('/payments/hold', {
152
- amount,
153
- idempotencyKey: idempotencyKey || randomIdempotencyKey('hold'),
154
- description,
155
- paymentGrantId,
156
- }));
157
- }
158
- /**
159
- * Choose the grant that actually covers this spend.
160
- *
161
- * This used to be `grants[0]` — the first active grant, matched against
162
- * nothing. An agent holding both its standing grant and an
163
- * agreement-conferred one presented whichever came back first, the backend
164
- * correctly refused it against wallet or amount, and the agent was refused a
165
- * spend it was genuinely authorized to make.
166
- *
167
- * The source wallet is read rather than assumed: a grant on some other
168
- * wallet cannot authorize money leaving this one, and the agent may hold
169
- * grants on a counterparty's wallet from an engagement. A failure to read it
170
- * is not fatal — selection then skips the wallet test and the server still
171
- * validates, which is the same position we were in before.
172
- */
173
- async selectPaymentGrant(spend) {
174
- let grants = [];
175
- try {
176
- grants = await this.listGrants();
177
- }
178
- catch {
179
- return { grant: null, rejected: [] };
180
- }
181
- let fromWalletId = null;
182
- try {
183
- fromWalletId = (await this.balance()).walletId;
184
- }
185
- catch {
186
- fromWalletId = null;
187
- }
188
- return selectPaymentGrant(grants, { ...spend, fromWalletId });
189
- }
190
- async release({ holdId, action = 'complete', toWalletId, idempotencyKey, }) {
191
- return (await this._post(`/payments/release/${holdId}`, {
192
- idempotencyKey: idempotencyKey || randomIdempotencyKey('rel'),
193
- action,
194
- toWalletId,
195
- }));
196
- }
197
64
  async history(params = {}) {
198
65
  const q = new URLSearchParams();
199
66
  if (params.limit != null)
@@ -237,68 +104,6 @@ export class PaymentsClient {
237
104
  description,
238
105
  });
239
106
  }
240
- async approvals({ status = 'pending' } = {}) {
241
- const res = (await this._get(`/payments/approvals?status=${encodeURIComponent(status)}`));
242
- return res['approvals'] || [];
243
- }
244
- async getApproval(approvalId) {
245
- if (!approvalId)
246
- throw new Error('getApproval: approvalId is required');
247
- try {
248
- const res = (await this._get(`/payments/approvals/${approvalId}`));
249
- return res?.['approval'] || null;
250
- }
251
- catch (err) {
252
- if (err.status === 404)
253
- return null;
254
- throw err;
255
- }
256
- }
257
- async decide(approvalId, decision, note = '') {
258
- return this._post(`/payments/approvals/${approvalId}/decide`, { decision, note });
259
- }
260
- async waitForApproval(approvalId, opts = {}) {
261
- const pollMs = Math.max(500, opts.pollMs ?? 3_000);
262
- const timeoutMs = Math.max(pollMs, opts.timeoutMs ?? 120_000);
263
- const signal = opts.signal;
264
- const deadline = Date.now() + timeoutMs;
265
- const sleep = (ms) => new Promise((resolve, reject) => {
266
- const timer = setTimeout(resolve, ms);
267
- if (signal) {
268
- if (signal.aborted) {
269
- clearTimeout(timer);
270
- reject(new Error('aborted'));
271
- return;
272
- }
273
- signal.addEventListener('abort', () => {
274
- clearTimeout(timer);
275
- reject(new Error('aborted'));
276
- }, { once: true });
277
- }
278
- });
279
- while (true) {
280
- if (signal?.aborted)
281
- throw new Error('aborted');
282
- const approval = await this.getApproval(approvalId);
283
- if (!approval)
284
- return { status: 'gone' };
285
- const s = approval.status;
286
- if (s === 'executed')
287
- return {
288
- status: 'executed',
289
- approval,
290
- transactionId: approval.executedTransactionId || null,
291
- };
292
- if (s === 'rejected')
293
- return { status: 'rejected', approval };
294
- if (s === 'expired')
295
- return { status: 'expired', approval };
296
- const remaining = deadline - Date.now();
297
- if (remaining <= 0)
298
- return { status: 'timeout', approval };
299
- await sleep(Math.min(pollMs, remaining));
300
- }
301
- }
302
107
  async _request(method, path, body) {
303
108
  const init = {
304
109
  method,
@@ -22,7 +22,7 @@ export type { ContextGrantRecord, ContextGrantScope, ContextGrantScopeKind, Cont
22
22
  export { grantCaveat, CONTEXT_GRANT_SCOPE_KINDS, GRANT_SCOPE_KINDS, } from './grants.js';
23
23
  export type { GrantView, GrantScopeKind, GrantScopeView, GrantCaveatView, GrantHealth, GrantHolderKind, GrantAccessKind, GrantBasisKind, } from './grants.js';
24
24
  export { PaymentsClient } from './PaymentsClient.js';
25
- export type { PaymentsError, WalletBalance, WalletRef, PaymentTransactionView, TransferPayee, TransferResult, HoldResult, ReleaseResult, PaymentGrantView, PaymentGrantEnvelope, RevokeGrantResult, PaymentApproval, WaitForApprovalResult, } from './PaymentsClient.js';
25
+ export type { PaymentsError, WalletBalance, WalletRef, PaymentGrantView, PaymentGrantEnvelope, RevokeGrantResult, } from './PaymentsClient.js';
26
26
  export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
27
27
  export type { ConnectionsError, ConnectionProxyParams, ConnectionGrant, ConnectionWithGrants, McpConnectionRequestParams, } from './ConnectionsClient.js';
28
28
  export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentAccess, fetchSessionAccess, } from './OrgsClient.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.34.1",
3
+ "version": "0.35.0",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,58 +0,0 @@
1
- import { type GrantView } from './grants.js';
2
- /**
3
- * Which grant to present for a spend, and — when none fits — why each candidate
4
- * did not.
5
- *
6
- * The client used to take `grants[0]` from the active list. It matched nothing
7
- * else: not the source wallet, not the amount, not the agreement the spend
8
- * belongs to. An agent holding both its standing grant and an agreement-conferred
9
- * one presented whichever came back first, the backend correctly refused it
10
- * against wallet or amount, and the agent was refused a spend it was genuinely
11
- * authorized to make.
12
- *
13
- * Selection is a pure function of the grants and the spend so it can be tested
14
- * without a server, and so the refusal can name what it considered.
15
- */
16
- export interface SpendShape {
17
- /** The wallet the money leaves. A grant on another wallet cannot authorize it. */
18
- fromWalletId?: string | null;
19
- /** Cents. Checked against `max_amount`. */
20
- amount: number;
21
- /** The wallet receiving it, when known. Checked against `allowed_recipients`. */
22
- toWalletId?: string | null;
23
- /** The engagement this spend belongs to, when the caller names one. */
24
- agreementId?: string | null;
25
- }
26
- export interface RejectedGrant {
27
- grantId: string;
28
- reason: string;
29
- }
30
- export interface GrantSelection {
31
- grant: GrantView | null;
32
- /** Every grant that was looked at and did not fit, with the reason. */
33
- rejected: RejectedGrant[];
34
- }
35
- /**
36
- * Pick the grant to present, preferring the one that belongs to the engagement.
37
- *
38
- * Order among grants that all fit: the agreement's own grant first when the
39
- * spend names one, then the standing grant. A standing grant is the buyer
40
- * bootstrap and the right default; reaching for it while an agreement grant
41
- * covers the same spend would bill the wrong budget.
42
- *
43
- * This is a preference, never a fence. Every grant here is already one the
44
- * server said this wake may spend, so picking a different one cannot reach
45
- * another party's authority — it would only bill the wrong budget of the same
46
- * party. That is why the mismatch is no longer a rejection above.
47
- */
48
- export declare function selectPaymentGrant(grants: readonly GrantView[], spend: SpendShape): GrantSelection;
49
- /**
50
- * The sentence an agent gets when nothing fits.
51
- *
52
- * It names every grant considered and why each was not it, because "no payment
53
- * grant" is a condition and an agent cannot act on a condition — it retries,
54
- * and every retry is a full model call. Which ceiling was in the way is the
55
- * thing that tells the agent whether to ask for a bigger grant, name the
56
- * agreement, or stop.
57
- */
58
- export declare function describeNoGrant(verb: string, spend: SpendShape, rejected: readonly RejectedGrant[]): string;
@@ -1,121 +0,0 @@
1
- import { grantCaveat } from './grants.js';
2
- /**
3
- * A caveat off a grant that may not carry the array at all.
4
- *
5
- * `grantCaveat` assumes `caveats` is populated, which the unified list read
6
- * does. A caller handing us a partial view should get "no such caveat", not a
7
- * crash — this runs on the spend path, where throwing would turn a missing
8
- * field into a refused payment.
9
- */
10
- function caveatValue(grant, type) {
11
- const caveats = grant.caveats;
12
- if (!Array.isArray(caveats))
13
- return undefined;
14
- return grantCaveat(grant, type);
15
- }
16
- function numericCaveat(grant, type) {
17
- const raw = caveatValue(grant, type);
18
- return typeof raw === 'number' && Number.isFinite(raw) ? raw : null;
19
- }
20
- function recipientCaveat(grant) {
21
- const raw = caveatValue(grant, 'allowed_recipients');
22
- if (!Array.isArray(raw))
23
- return null;
24
- const ids = raw.filter((r) => typeof r === 'string');
25
- return ids.length ? ids : null;
26
- }
27
- /**
28
- * Why this grant cannot authorize this spend, or null if it can.
29
- *
30
- * `daily_budget` is deliberately not checked. It is a ceiling on spend already
31
- * made, which only the server can see — guessing here would refuse a grant that
32
- * would have worked. The server validates it inside the same transaction as the
33
- * ledger write, which is the only place the answer is not already stale.
34
- */
35
- function whyNot(grant, spend) {
36
- // A stated kind that is not `wallet` is disqualifying. An ABSENT one is not:
37
- // `listGrants` asks the server for `scopeKind: 'wallet'`, so the rail has
38
- // already answered, and treating a field the response did not populate as a
39
- // wrong answer would refuse every grant. Same reading as an unread source
40
- // wallet below — absence means unknown, never no.
41
- if (grant.scope?.kind && grant.scope.kind !== 'wallet') {
42
- return `is a ${grant.scope.kind} grant, not a wallet grant`;
43
- }
44
- if (spend.fromWalletId && grant.scope?.id && grant.scope.id !== spend.fromWalletId) {
45
- return `is on wallet ${grant.scope.id}, and this spend leaves ${spend.fromWalletId}`;
46
- }
47
- const maxAmount = numericCaveat(grant, 'max_amount');
48
- if (maxAmount !== null && spend.amount > maxAmount) {
49
- return `caps a single spend at ${maxAmount}, and this one is ${spend.amount}`;
50
- }
51
- const allowed = recipientCaveat(grant);
52
- if (allowed && spend.toWalletId && !allowed.includes(spend.toWalletId)) {
53
- return `may only pay ${allowed.join(', ')}, and this one pays ${spend.toWalletId}`;
54
- }
55
- // A grant's agreement is NOT checked here, deliberately.
56
- //
57
- // It used to be refused when it did not match the spend's agreement, which
58
- // made a customer's own grant unusable inside their second hire of the same
59
- // agent, and made a standing grant unusable on any spend that named a deal.
60
- // Whether a grant may be spent at all is the server's answer, and it turns on
61
- // who GAVE the grant against who the agent is acting for — not on which of
62
- // that party's deals is running. A list the server answered for this wake
63
- // holds only grants it may spend, so there is nothing left to refuse here.
64
- //
65
- // The agreement still decides the ORDER below, which is a budget preference
66
- // rather than a question of authority.
67
- return null;
68
- }
69
- /**
70
- * Pick the grant to present, preferring the one that belongs to the engagement.
71
- *
72
- * Order among grants that all fit: the agreement's own grant first when the
73
- * spend names one, then the standing grant. A standing grant is the buyer
74
- * bootstrap and the right default; reaching for it while an agreement grant
75
- * covers the same spend would bill the wrong budget.
76
- *
77
- * This is a preference, never a fence. Every grant here is already one the
78
- * server said this wake may spend, so picking a different one cannot reach
79
- * another party's authority — it would only bill the wrong budget of the same
80
- * party. That is why the mismatch is no longer a rejection above.
81
- */
82
- export function selectPaymentGrant(grants, spend) {
83
- const rejected = [];
84
- const fitting = [];
85
- for (const grant of grants) {
86
- const reason = whyNot(grant, spend);
87
- if (reason)
88
- rejected.push({ grantId: grant.grantId, reason });
89
- else
90
- fitting.push(grant);
91
- }
92
- if (fitting.length === 0)
93
- return { grant: null, rejected };
94
- // Explicit both ways, because the deal is no longer a fence: until it was
95
- // removed, a deal-bound grant simply never reached this list on a spend that
96
- // named nothing, so "prefer the standing grant" happened by accident of the
97
- // rejection. Now it has to be said.
98
- const standing = fitting.find((g) => !g.agreementId);
99
- const preferred = spend.agreementId
100
- ? (fitting.find((g) => g.agreementId === spend.agreementId) ?? standing)
101
- : standing;
102
- return { grant: preferred ?? fitting[0], rejected };
103
- }
104
- /**
105
- * The sentence an agent gets when nothing fits.
106
- *
107
- * It names every grant considered and why each was not it, because "no payment
108
- * grant" is a condition and an agent cannot act on a condition — it retries,
109
- * and every retry is a full model call. Which ceiling was in the way is the
110
- * thing that tells the agent whether to ask for a bigger grant, name the
111
- * agreement, or stop.
112
- */
113
- export function describeNoGrant(verb, spend, rejected) {
114
- if (rejected.length === 0) {
115
- return (`${verb}: you hold no active payment grant, so this spend cannot be ` +
116
- `authorized. The wallet owner issues one; nothing you can do from here.`);
117
- }
118
- const lines = rejected.map((r) => ` - ${r.grantId} ${r.reason}`).join('\n');
119
- return (`${verb}: none of your ${rejected.length} active payment grant(s) covers ` +
120
- `this spend of ${spend.amount}.\n${lines}`);
121
- }