@ziggs-ai/api-client 0.10.4 → 0.12.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 (51) hide show
  1. package/dist/capabilities/agreementVerbs.d.ts +9 -2
  2. package/dist/capabilities/agreementVerbs.js +61 -21
  3. package/dist/capabilities/agreements.d.ts +1 -1
  4. package/dist/capabilities/agreements.js +18 -6
  5. package/dist/capabilities/artifacts.d.ts +4 -2
  6. package/dist/capabilities/artifacts.js +168 -33
  7. package/dist/capabilities/chat.d.ts +2 -3
  8. package/dist/capabilities/chat.js +5 -6
  9. package/dist/capabilities/connections.js +1 -1
  10. package/dist/capabilities/context.js +3 -0
  11. package/dist/capabilities/grants.d.ts +7 -6
  12. package/dist/capabilities/grants.js +9 -8
  13. package/dist/capabilities/index.d.ts +4 -4
  14. package/dist/capabilities/index.js +4 -4
  15. package/dist/capabilities/links.d.ts +17 -6
  16. package/dist/capabilities/links.js +71 -86
  17. package/dist/capabilities/marketplace.js +23 -17
  18. package/dist/capabilities/nextCall.d.ts +36 -5
  19. package/dist/capabilities/nextCall.js +53 -7
  20. package/dist/capabilities/payments.d.ts +24 -8
  21. package/dist/capabilities/payments.js +28 -392
  22. package/dist/capabilities/proposeProviderId.d.ts +1 -1
  23. package/dist/capabilities/proposeProviderId.js +1 -1
  24. package/dist/http/AgreementClient.d.ts +63 -27
  25. package/dist/http/AgreementClient.js +51 -39
  26. package/dist/http/ChatClient.d.ts +1 -0
  27. package/dist/http/ChatClient.js +4 -1
  28. package/dist/http/ConnectionsClient.js +12 -1
  29. package/dist/http/ContextGrantsClient.d.ts +15 -1
  30. package/dist/http/ContextGrantsClient.js +2 -0
  31. package/dist/http/ContextReadClient.d.ts +14 -5
  32. package/dist/http/GrantsClient.d.ts +14 -0
  33. package/dist/http/GrantsClient.js +18 -2
  34. package/dist/http/InboxClient.js +4 -0
  35. package/dist/http/MarketplaceClient.d.ts +6 -8
  36. package/dist/http/MarketplaceClient.js +11 -30
  37. package/dist/http/TaskClient.d.ts +5 -0
  38. package/dist/http/TaskClient.js +4 -7
  39. package/dist/http/agreementFlows.d.ts +6 -7
  40. package/dist/http/agreementFlows.js +14 -20
  41. package/dist/http/grants.d.ts +28 -0
  42. package/dist/http/index.d.ts +2 -2
  43. package/dist/index.d.ts +3 -3
  44. package/dist/index.js +1 -1
  45. package/dist/instanceIdentity.d.ts +4 -0
  46. package/dist/instanceIdentity.js +44 -0
  47. package/dist/relay/provisionRelayWorkers.d.ts +2 -2
  48. package/dist/relay/provisionRelayWorkers.js +5 -5
  49. package/dist/types.d.ts +80 -31
  50. package/dist/types.js +18 -0
  51. package/package.json +1 -1
@@ -1,8 +1,15 @@
1
1
  import type { CapabilityDefinition } from './types.js';
2
- export declare const agreementCommissionCapability: CapabilityDefinition;
2
+ export declare const agreementBuyCapability: CapabilityDefinition;
3
3
  export declare const agreementBidCapability: CapabilityDefinition;
4
4
  export declare const agreementBrokerCapability: CapabilityDefinition;
5
- export declare const agreementQuestCapability: CapabilityDefinition;
5
+ export declare const agreementRequestCapability: CapabilityDefinition;
6
+ /**
7
+ * No mandate param here, deliberately: a listing is formed inside no job. It is
8
+ * a standing invitation to the world that outlives whatever the agent happens
9
+ * to be doing today, and the publish path says so at its own end
10
+ * (`POST /marketplace/offers/publish`). Hanging one under a job would kill the
11
+ * listing when the job ended.
12
+ */
6
13
  export declare const agreementOfferCapability: CapabilityDefinition;
7
14
  export declare const agreementHandoffCapability: CapabilityDefinition;
8
15
  export declare const AGREEMENT_VERB_CAPABILITIES: CapabilityDefinition[];
@@ -30,7 +30,7 @@ const TERMS_PARAMS = {
30
30
  },
31
31
  price: {
32
32
  type: 'number',
33
- description: 'Amount in CENTS — 500 means $5.00, and ϟ5.00 in the UI. Optional; recording a price does not itself move money. Convert BOTH ways or you are off by 100x: a human who says "ϟ2" means 200, and quoting 500 back as "ϟ500" is the same mistake inverted.',
33
+ description: 'Price in POINTS, as an integer of hundredths — 500 means ϟ5.00. Recording a price here does not itself move any. Convert BOTH ways or you are off by 100x: a human who says "ϟ2" means 200, and quoting 500 back as "ϟ500" is the same mistake inverted.',
34
34
  },
35
35
  billing: {
36
36
  type: 'string',
@@ -77,6 +77,28 @@ function termsFrom(args) {
77
77
  function engagementKindFrom(args) {
78
78
  return args['engagementKind'] ?? 'service';
79
79
  }
80
+ /**
81
+ * The job an agent declares it is acting inside.
82
+ *
83
+ * The consent gate excuses a formation that sits inside a job a human already
84
+ * approved, and it verifies the claim against the acting agent. What did not
85
+ * exist was a way to MAKE the claim: `parentAgreementId` rode only on internal
86
+ * service calls, so every agreement an agent formed arrived at the gate as an
87
+ * outsider and waited on a human — including the ordinary case of buying the
88
+ * help the job it was hired for needs.
89
+ *
90
+ * Declared by the caller, never guessed by the server. The SDK fills it in from
91
+ * the task it is executing, so an agent doing assigned work pays nothing for it.
92
+ */
93
+ const MANDATE_PARAM = {
94
+ type: 'string',
95
+ description: 'The active agreement whose work this is part of — the job you are doing. It becomes this engagement\'s parent, so it ends when the job ends, and it is what lets you commit without waiting on your human: work inside a job they already approved needs no second approval. Name only an agreement you are actually a party to; the server checks, and a wrong name simply earns nothing. Leave it out for work that belongs to no job.',
96
+ };
97
+ /** Pull the declared mandate off a validated arg bag, as the wire field. */
98
+ function mandateFrom(args) {
99
+ const declared = args['mandateAgreementId'];
100
+ return typeof declared === 'string' && declared ? { parentAgreementId: declared } : {};
101
+ }
80
102
  /** The audience a broadcast reaches. */
81
103
  const AUDIENCE_PARAM = {
82
104
  type: 'string',
@@ -101,25 +123,27 @@ function publishedNext(env) {
101
123
  ];
102
124
  }
103
125
  /* ─────────────────────────── direct, to one counterparty ─────────────────── */
104
- export const agreementCommissionCapability = {
105
- key: 'agreement_commission',
106
- names: { sdk: 'agreement_commission', mcp: 'ziggs_agreement_commission' },
107
- title: 'Commission a counterparty',
126
+ export const agreementBuyCapability = {
127
+ key: 'agreement_buy',
128
+ names: { sdk: 'agreement_buy', mcp: 'ziggs_agreement_buy' },
129
+ title: 'They work, you pay (named counterparty)',
108
130
  descriptions: {
109
- 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_quest.',
110
- 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_quest.',
131
+ 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.',
132
+ 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.',
111
133
  },
112
134
  annotation: 'write',
113
135
  params: {
114
136
  counterparty: COUNTERPARTY_PARAM,
115
137
  chatId: CHAT_PARAM,
116
138
  ...TERMS_PARAMS,
139
+ mandateAgreementId: MANDATE_PARAM,
117
140
  },
118
141
  needsAgentId: true,
119
142
  handler: async (args, env) => {
120
143
  const counterparty = args['counterparty'];
121
144
  const agreement = await proposeDirectTo({
122
145
  ...termsFrom(args),
146
+ ...mandateFrom(args),
123
147
  proposedTo: counterparty,
124
148
  chatId: args['chatId'],
125
149
  // They provide. The payer is derived server-side as the other side.
@@ -133,22 +157,24 @@ export const agreementCommissionCapability = {
133
157
  export const agreementBidCapability = {
134
158
  key: 'agreement_bid',
135
159
  names: { sdk: 'agreement_bid', mcp: 'ziggs_agreement_bid' },
136
- title: 'Offer to work for a counterparty',
160
+ title: 'You work, they pay (named counterparty)',
137
161
  descriptions: {
138
162
  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.',
139
- 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_commission.',
163
+ 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.',
140
164
  },
141
165
  annotation: 'write',
142
166
  params: {
143
167
  counterparty: COUNTERPARTY_PARAM,
144
168
  chatId: CHAT_PARAM,
145
169
  ...TERMS_PARAMS,
170
+ mandateAgreementId: MANDATE_PARAM,
146
171
  },
147
172
  needsAgentId: true,
148
173
  handler: async (args, env) => {
149
174
  const creds = fullCreds(env);
150
175
  const agreement = await proposeDirectTo({
151
176
  ...termsFrom(args),
177
+ ...mandateFrom(args),
152
178
  proposedTo: args['counterparty'],
153
179
  chatId: args['chatId'],
154
180
  // You provide, so the counterparty pays.
@@ -162,7 +188,7 @@ export const agreementBidCapability = {
162
188
  export const agreementBrokerCapability = {
163
189
  key: 'agreement_broker',
164
190
  names: { sdk: 'agreement_broker', mcp: 'ziggs_agreement_broker' },
165
- title: 'Broker work between two other parties',
191
+ title: 'A third party does the work, you arrange it',
166
192
  descriptions: {
167
193
  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.',
168
194
  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.',
@@ -180,11 +206,13 @@ export const agreementBrokerCapability = {
180
206
  },
181
207
  chatId: CHAT_PARAM,
182
208
  ...TERMS_PARAMS,
209
+ mandateAgreementId: MANDATE_PARAM,
183
210
  },
184
211
  needsAgentId: true,
185
212
  handler: async (args, env) => {
186
213
  const agreement = await proposeDirectTo({
187
214
  ...termsFrom(args),
215
+ ...mandateFrom(args),
188
216
  proposedTo: args['counterparty'],
189
217
  chatId: args['chatId'],
190
218
  providerId: args['provider'],
@@ -195,20 +223,25 @@ export const agreementBrokerCapability = {
195
223
  sdkOptions: { isAgreementCreation: true },
196
224
  };
197
225
  /* ──────────────────────────────── broadcast ──────────────────────────────── */
198
- export const agreementQuestCapability = {
199
- key: 'agreement_quest',
200
- names: { sdk: 'agreement_quest', mcp: 'ziggs_agreement_quest' },
201
- title: 'Post a quest',
226
+ export const agreementRequestCapability = {
227
+ key: 'agreement_request',
228
+ names: { sdk: 'agreement_request', mcp: 'ziggs_agreement_request' },
229
+ title: 'They work, you pay (open to anyone)',
202
230
  descriptions: {
203
231
  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.',
204
232
  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.',
205
233
  },
206
234
  annotation: 'write',
207
- params: { audience: AUDIENCE_PARAM, ...TERMS_PARAMS },
235
+ params: {
236
+ audience: AUDIENCE_PARAM,
237
+ ...TERMS_PARAMS,
238
+ mandateAgreementId: MANDATE_PARAM,
239
+ },
208
240
  needsAgentId: true,
209
241
  handler: async (args, env) => {
210
242
  const agreement = await proposeBroadcast({
211
243
  ...termsFrom(args),
244
+ ...mandateFrom(args),
212
245
  chatId: '',
213
246
  audience: args['audience'],
214
247
  engagementKind: engagementKindFrom(args),
@@ -217,13 +250,20 @@ export const agreementQuestCapability = {
217
250
  },
218
251
  sdkOptions: { isAgreementCreation: true },
219
252
  };
253
+ /**
254
+ * No mandate param here, deliberately: a listing is formed inside no job. It is
255
+ * a standing invitation to the world that outlives whatever the agent happens
256
+ * to be doing today, and the publish path says so at its own end
257
+ * (`POST /marketplace/offers/publish`). Hanging one under a job would kill the
258
+ * listing when the job ended.
259
+ */
220
260
  export const agreementOfferCapability = {
221
261
  key: 'agreement_offer',
222
262
  names: { sdk: 'agreement_offer', mcp: 'ziggs_agreement_offer' },
223
- title: 'Publish a standing offer',
263
+ title: 'You work, they pay (open to anyone)',
224
264
  descriptions: {
225
265
  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.',
226
- 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_quest.',
266
+ 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.',
227
267
  },
228
268
  annotation: 'write',
229
269
  params: { audience: AUDIENCE_PARAM, ...TERMS_PARAMS },
@@ -248,7 +288,7 @@ export const agreementOfferCapability = {
248
288
  export const agreementHandoffCapability = {
249
289
  key: 'agreement_handoff',
250
290
  names: { sdk: 'agreement_handoff', mcp: 'ziggs_agreement_handoff' },
251
- title: 'Hand off an agent you hired',
291
+ title: 'Pass a hire you hold to whoever claims it',
252
292
  descriptions: {
253
293
  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.',
254
294
  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.',
@@ -278,7 +318,7 @@ export const agreementHandoffCapability = {
278
318
  // would have had to look it up to pass it, and a lookup whose answer is
279
319
  // unique is not a decision worth handing to the caller.
280
320
  const parent = await getAgreement(parentAgreementId, creds);
281
- const providerId = parent?.parties?.providerAgent;
321
+ const providerId = parent?.parties?.provider?.actor;
282
322
  if (!providerId) {
283
323
  throw new Error(`Cannot hand off ${parentAgreementId}: it names no provider, so there is no hire to share. Check the id with agreement_get.`);
284
324
  }
@@ -296,10 +336,10 @@ export const agreementHandoffCapability = {
296
336
  sdkOptions: { isAgreementCreation: true },
297
337
  };
298
338
  export const AGREEMENT_VERB_CAPABILITIES = [
299
- agreementCommissionCapability,
339
+ agreementBuyCapability,
300
340
  agreementBidCapability,
301
341
  agreementBrokerCapability,
302
- agreementQuestCapability,
342
+ agreementRequestCapability,
303
343
  agreementOfferCapability,
304
344
  agreementHandoffCapability,
305
345
  ];
@@ -1,6 +1,6 @@
1
1
  import { type CapabilityDefinition } from './types.js';
2
2
  /**
3
- * the one claim verb. Quests, standing offers, and link invites
3
+ * the one claim verb. Requests, standing offers, and link invites
4
4
  * are all open broadcasts; claiming any of them is this call. The respond
5
5
  * tool no longer claims — it approves/rejects direct proposals only.
6
6
  */
@@ -2,7 +2,7 @@ import { claimOpenAgreement } from '../http/agreementFlows.js';
2
2
  import { linkIsReachOnly } from './links.js';
3
3
  import { fullCreds } from './types.js';
4
4
  /**
5
- * the one claim verb. Quests, standing offers, and link invites
5
+ * the one claim verb. Requests, standing offers, and link invites
6
6
  * are all open broadcasts; claiming any of them is this call. The respond
7
7
  * tool no longer claims — it approves/rejects direct proposals only.
8
8
  */
@@ -11,20 +11,32 @@ export const agreementClaimCapability = {
11
11
  names: { sdk: 'agreement_claim', mcp: 'ziggs_agreement_claim' },
12
12
  title: 'Claim a posted agreement',
13
13
  descriptions: {
14
- sdk: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim, activation is instant, no negotiation turns. Claims a quest (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 and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find quests/offers with marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with agreement_respond instead, not claimed.',
15
- mcp: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim, activation is instant, no negotiation turns. Claims a quest (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 and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find quests/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.',
14
+ sdk: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim, activation is instant, 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 and the agreement activates. 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.',
15
+ mcp: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim, activation is instant, 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 and the agreement activates. 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.',
16
16
  },
17
17
  annotation: 'write',
18
18
  params: {
19
19
  agreementId: {
20
20
  type: 'string',
21
21
  required: true,
22
- description: 'The open agreement to claim (quest / offer / link invite id)',
22
+ description: 'The open agreement to claim (request / offer / link invite id)',
23
+ },
24
+ // The job this claim is part of. A claim is the DEFAULT way to engage, so
25
+ // without this the ordinary case — an agent claiming the help the job it is
26
+ // doing needs — waited on a human every time. It authorizes; it does not
27
+ // re-parent: the row belongs to whoever posted it, and a claimer cannot move
28
+ // somebody else's agreement into its own tree.
29
+ mandateAgreementId: {
30
+ type: 'string',
31
+ description: 'The active agreement whose work this claim is part of — the job you are doing. Claiming for a job your human already approved needs no second approval from them, but you have to name the job: the server checks you are a party to it, and a wrong name simply earns nothing. Leave it out when the claim belongs to no job.',
23
32
  },
24
33
  },
25
34
  needsAgentId: true,
26
35
  handler: async (args, env) => {
27
- const { agreement, kind } = await claimOpenAgreement(args['agreementId'], fullCreds(env));
36
+ const declaredMandate = args['mandateAgreementId'];
37
+ const { agreement, kind } = await claimOpenAgreement(args['agreementId'], fullCreds(env), typeof declaredMandate === 'string' && declaredMandate
38
+ ? { mandateAgreementId: declaredMandate }
39
+ : {});
28
40
  if (kind === 'link') {
29
41
  return {
30
42
  status: 'linked',
@@ -40,7 +52,7 @@ export const agreementClaimCapability = {
40
52
  ? 'Standing offer claimed — the publisher provides, your side pays. Spawn work under it with the task-create tool.'
41
53
  : kind === 'hand-off'
42
54
  ? 'Hand-off claimed — the pinned agent works FOR you: you are the customer (and the payer when priced), never the worker. Spawn work under it with the task-create tool.'
43
- : 'Quest claimed — you provide the work. Read the terms, then post progress and set the task result under this agreement.',
55
+ : 'Request claimed — you provide the work. Read the terms, then post progress and set the task result under this agreement.',
44
56
  agreement,
45
57
  };
46
58
  },
@@ -29,8 +29,10 @@ export declare const shareArtifactCapability: CapabilityDefinition;
29
29
  */
30
30
  export declare const attachArtifactCapability: CapabilityDefinition;
31
31
  /**
32
- * File deliverables (presign → PUT → complete). Text `artifact_record` stays
33
- * for text bodies; use this rail when the payload is a file on object storage.
32
+ * The escape hatch for bytes too big to travel inside a tool call
33
+ * (the remote MCP transport caps a request body at 1MB). The ordinary way to
34
+ * record a file is `artifact_record` with contentBase64, which runs this presign
35
+ * → PUT → complete sequence internally.
34
36
  */
35
37
  export declare const uploadArtifactUrlCapability: CapabilityDefinition;
36
38
  export declare const completeArtifactFileCapability: CapabilityDefinition;
@@ -18,33 +18,153 @@ function reportingHint(env, contentType, taskId) {
18
18
  return `Reporting finished work? Record it with contentType=result bound to the task (taskId), then close the task with ${close} — chat messages are conversation only.`;
19
19
  }
20
20
  /** Inline body cap + escape hatch, shared by SDK/MCP descriptions. */
21
- const ARTIFACT_RECORD_INLINE_CAP = 'Inline text max 50000 characters. Over that: use ziggs_artifact_upload_url ' +
22
- '(file rail), or record an index artifact plus part artifacts and list the ' +
23
- 'part ids in the index. The server does not auto-split.';
21
+ const ARTIFACT_RECORD_INLINE_CAP = 'Inline text max 50000 characters. Over that: pass the body as a file ' +
22
+ '(filename + mime + contentBase64, handled in this one call), or record an ' +
23
+ 'index artifact plus part artifacts and list the part ids in the index. ' +
24
+ 'The server does not auto-split.';
25
+ /**
26
+ * The file rail, as one call.
27
+ *
28
+ * Recording a file used to be three steps a caller had to perform in order:
29
+ * ask for a presigned URL, PUT the bytes to object storage, then report the
30
+ * sha256 back. That is our storage design surfaced as a caller obligation —
31
+ * one of those two tools spent 1,421 characters explaining it — and every
32
+ * agent paid for both schemas in every session whether or not it ever wrote a
33
+ * file. Hand `contentBase64` here and the presign, the PUT and the completion
34
+ * all happen inside this handler.
35
+ *
36
+ * `artifact_upload_url` / `artifact_complete_file` still exist for the one case
37
+ * that genuinely needs them: bytes too large to travel inside a tool call (the
38
+ * remote MCP transport caps a request body at 1MB). They are no longer the
39
+ * documented path.
40
+ */
41
+ /**
42
+ * The file's bytes, or a refusal that says the payload arrived damaged.
43
+ *
44
+ * `Buffer.from(s, 'base64')` does not validate: it silently drops every
45
+ * character it does not recognise and stops at the first bad padding, so a
46
+ * truncated payload — the ordinary failure when a model inlines a long file —
47
+ * decodes to FEWER bytes and everything downstream agrees with itself. The
48
+ * sha256 is taken over what survived, the upload completes, and the caller is
49
+ * told the file stored. Nothing surfaces until someone reads a garbled extract
50
+ * with no way back to the call that wrote it.
51
+ *
52
+ * Re-encoding is the cheap question Node will not ask: if every character
53
+ * carried data, the bytes round-trip to the same string. Whitespace is stripped
54
+ * first (wrapping is normal in transports) and base64url is folded into
55
+ * standard base64, so only genuine corruption is refused.
56
+ */
57
+ function decodeFileBytes(contentBase64) {
58
+ const normalized = contentBase64
59
+ .replace(/\s+/g, '')
60
+ .replace(/-/g, '+')
61
+ .replace(/_/g, '/')
62
+ .replace(/=+$/, '');
63
+ const bytes = Buffer.from(normalized, 'base64');
64
+ if (bytes.byteLength < 1) {
65
+ throw new Error('contentBase64 decoded to zero bytes — pass the base64 of the file, or use text for a text artifact.');
66
+ }
67
+ if (bytes.toString('base64').replace(/=+$/, '') !== normalized) {
68
+ throw new Error(`contentBase64 is not intact base64: ${normalized.length} characters decoded to ${bytes.byteLength} bytes and do not re-encode to what was sent, so part of the payload was dropped. ` +
69
+ 'Send the whole encoding — storing it would give you a smaller, corrupt file whose checksum matches the corruption. ' +
70
+ 'Bytes over ~1MB do not fit in a tool call at all: use artifact_upload_url for those.');
71
+ }
72
+ return bytes;
73
+ }
74
+ async function recordFileArtifact(args, env, visibility) {
75
+ const filename = args['filename']?.trim();
76
+ const mime = args['mime']?.trim();
77
+ if (!filename)
78
+ throw new Error('filename is required when passing contentBase64');
79
+ if (!mime)
80
+ throw new Error('mime is required when passing contentBase64');
81
+ const contentBase64 = args['contentBase64'];
82
+ // Decoded once, here, and handed on as bytes: `uploadUrl` takes `content`
83
+ // directly, so passing the string instead made every file call decode the
84
+ // same payload twice — and a stdio caller has no 1MB cap, so "the payload"
85
+ // can be tens of megabytes.
86
+ const bytes = decodeFileBytes(contentBase64);
87
+ const creds = fullCreds(env);
88
+ const client = new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId);
89
+ const taskId = args['taskId'];
90
+ const byteSize = bytes.byteLength;
91
+ const uploaded = await client.uploadUrl({
92
+ filename,
93
+ mime,
94
+ // The byte length of what will be PUT, not of the base64 envelope — the
95
+ // presign signs this number and S3 rejects a mismatch.
96
+ byteSize,
97
+ format: args['format'],
98
+ visibility,
99
+ chatId: args['chatId'],
100
+ agreementId: args['agreementId'],
101
+ taskId,
102
+ idempotencyKey: args['idempotencyKey'],
103
+ // ArtifactsClient does the PUT and hands back the sha256 of what it sent,
104
+ // so the checksum is never something the caller has to compute.
105
+ content: bytes,
106
+ });
107
+ if (!uploaded.checksum) {
108
+ throw new Error(`Uploaded ${filename} but the storage layer returned no checksum, so the artifact is incomplete. ` +
109
+ `Finish it with artifact_complete_file (artifactId ${uploaded.artifactId}) once you have the sha256 of the bytes.`);
110
+ }
111
+ const artifact = await client.completeFile(uploaded.artifactId, uploaded.checksum);
112
+ return {
113
+ ok: true,
114
+ artifactId: uploaded.artifactId,
115
+ artifact,
116
+ visibility,
117
+ chatId: args['chatId'],
118
+ agreementId: args['agreementId'],
119
+ taskId,
120
+ byteSize,
121
+ note: 'File stored and completed. Text extraction runs asynchronously — when extractionStatus is ok, read the extracted text rather than downloading the bytes again.',
122
+ reportingHint: reportingHint(env, args['contentType'], taskId),
123
+ };
124
+ }
24
125
  export const recordArtifactCapability = {
25
126
  key: 'artifact_record',
26
127
  names: { sdk: 'artifact_record', mcp: 'ziggs_artifact_record' },
27
128
  title: 'Record an artifact',
28
129
  descriptions: {
29
- sdk: 'Write an artifact. Scope is optional: pass agreementId or chatId to record it there, ' +
130
+ sdk: 'Write an artifact — text (text) or a file (filename + mime + contentBase64; the upload ' +
131
+ 'happens inside this call). Scope is optional: pass agreementId or chatId to record it there, ' +
30
132
  'or pass no scope at all to record it as yours alone and attach it somewhere later. ' +
31
133
  'Set visibility explicitly. For a finished deliverable, set contentType=result and pass ' +
32
- 'taskId to bind it to the task — heavy results belong in artifacts, not chat messages. ' +
134
+ 'taskId to bind it to the task. A heavy deliverable belongs in an artifact rather than pasted into a message. ' +
33
135
  ARTIFACT_RECORD_INLINE_CAP,
34
- mcp: 'Write an artifact. Scope is optional — pass agreementId or chatId to record it into that ' +
136
+ mcp: 'Write an artifact — text (text) or a file (filename + mime + contentBase64; presign, upload ' +
137
+ 'and completion all happen inside this one call, so there is no separate upload dance). ' +
138
+ 'Scope is optional — pass agreementId or chatId to record it into that ' +
35
139
  'scope, pass taskId alone to bind a deliverable to its task, or pass no scope at all for a ' +
36
140
  'free-standing artifact that is yours until you attach or share it. Never guess a scope: ' +
37
141
  'recording with none always succeeds. Set visibility explicitly. ' +
38
142
  'For a finished deliverable, set contentType=result and pass taskId to bind it to the task. ' +
39
- 'Finished work is the task result — set it with ziggs_task_set_result; never report finished work as a chat message (chat is conversation only). ' +
143
+ 'When the work rides a task, close it with ziggs_task_set_result too: that is what the next agent reads from its own inbox. ' +
40
144
  ARTIFACT_RECORD_INLINE_CAP,
41
145
  },
42
146
  annotation: 'write',
43
147
  params: {
44
148
  text: {
45
149
  type: 'string',
46
- required: true,
47
- description: 'Artifact body (max 50000 chars). Over limit: ziggs_artifact_upload_url or index+parts.',
150
+ description: 'Artifact body for a text artifact (max 50000 chars). Pass this or contentBase64, not both.',
151
+ },
152
+ contentBase64: {
153
+ type: 'string',
154
+ description: 'Base64 file bytes for a file artifact — requires filename and mime. This call presigns, uploads and completes; you never compute a checksum. Bytes over ~1MB will not fit in a tool call: use artifact_upload_url for those.',
155
+ },
156
+ filename: {
157
+ type: 'string',
158
+ description: 'Original filename — required with contentBase64',
159
+ },
160
+ mime: {
161
+ type: 'string',
162
+ description: 'Content-Type of the file bytes (e.g. application/pdf) — required with contentBase64',
163
+ },
164
+ format: {
165
+ type: 'string',
166
+ enum: ['pdf', 'docx', 'hwpx', 'md', 'txt', 'html'],
167
+ description: 'Optional file format; inferred from filename when omitted',
48
168
  },
49
169
  visibility: {
50
170
  type: 'string',
@@ -89,11 +209,24 @@ export const recordArtifactCapability = {
89
209
  if (visibility !== 'chat' && visibility !== 'agent-private') {
90
210
  throw new Error('visibility must be chat or agent-private');
91
211
  }
212
+ // One tool, two body forms. Both at once is refused rather
213
+ // than resolved: which one won would be invisible in the response.
214
+ const text = args['text'];
215
+ const contentBase64 = args['contentBase64'];
216
+ if (text != null && contentBase64 != null) {
217
+ throw new Error('Pass text or contentBase64, not both');
218
+ }
219
+ if (contentBase64 != null) {
220
+ return recordFileArtifact(args, env, visibility);
221
+ }
222
+ if (text == null) {
223
+ throw new Error('Pass text for a text artifact, or contentBase64 (with filename and mime) for a file');
224
+ }
92
225
  const contentType = args['contentType'];
93
226
  const taskId = args['taskId'];
94
227
  const creds = fullCreds(env);
95
228
  const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).writeStrict({
96
- text: args['text'],
229
+ text,
97
230
  visibility,
98
231
  chatId,
99
232
  agreementId,
@@ -284,26 +417,27 @@ export const attachArtifactCapability = {
284
417
  },
285
418
  };
286
419
  /**
287
- * File deliverables (presign → PUT → complete). Text `artifact_record` stays
288
- * for text bodies; use this rail when the payload is a file on object storage.
420
+ * The escape hatch for bytes too big to travel inside a tool call
421
+ * (the remote MCP transport caps a request body at 1MB). The ordinary way to
422
+ * record a file is `artifact_record` with contentBase64, which runs this presign
423
+ * → PUT → complete sequence internally.
289
424
  */
290
425
  export const uploadArtifactUrlCapability = {
291
426
  key: 'artifact_upload_url',
292
427
  names: { sdk: 'artifact_upload_url', mcp: 'ziggs_artifact_upload_url' },
293
- title: 'Start a file upload',
428
+ title: 'Start a large file upload',
294
429
  descriptions: {
295
- sdk: 'Start a file artifact upload. Returns a short-lived uploadUrl. PUT the exact ' +
296
- 'byteSize bytes to that URL (Content-Type = mime), then call artifact_complete_file ' +
297
- 'with the sha256 hex of those bytes. Pass contentBase64 (or have the runtime supply ' +
298
- 'content) to have this tool PUT for you and return checksum. When extractionStatus ' +
299
- 'becomes ok, read extracted text via context_read / the extracted-text child — not ' +
300
- 'by stuffing file bytes into artifact_record.',
301
- mcp: 'Start a file artifact upload (pdf/docx/hwpx/md/txt/html). Returns uploadUrl + artifactId. ' +
302
- 'PUT the file bytes to uploadUrl with the declared mime, then call ' +
303
- 'ziggs_artifact_complete_file with sha256 hex. Prefer contentBase64 so this tool PUTs ' +
304
- 'and returns checksum for you. Scope optional (chatId / agreementId / taskId) like ' +
305
- 'ziggs_artifact_record. Do not put large file bodies into ziggs_artifact_record. ' +
306
- 'After extract finishes (extractionStatus=ok), read text from context / the derived ' +
430
+ sdk: 'Large-file escape hatch. Ordinary files go through artifact_record (filename + mime + ' +
431
+ 'contentBase64), which does all of this in one call — use this only when the bytes are too ' +
432
+ 'large to travel inside a tool call. Returns a short-lived uploadUrl: PUT the exact byteSize ' +
433
+ 'bytes to it (Content-Type = mime), then call artifact_complete_file with the sha256 hex of ' +
434
+ 'those bytes.',
435
+ mcp: 'Large-file escape hatch (pdf/docx/hwpx/md/txt/html). Ordinary files go through ' +
436
+ 'ziggs_artifact_record (filename + mime + contentBase64), which presigns, uploads and ' +
437
+ 'completes in one call — use this only when the bytes are too large to travel inside a tool ' +
438
+ 'call. Returns uploadUrl + artifactId: PUT the bytes with the declared mime, then call ' +
439
+ 'ziggs_artifact_complete_file with sha256 hex. Scope optional (chatId / agreementId / ' +
440
+ 'taskId). After extract finishes (extractionStatus=ok), read text from context / the derived ' +
307
441
  'extracted-text artifact.',
308
442
  },
309
443
  annotation: 'write',
@@ -387,15 +521,16 @@ export const uploadArtifactUrlCapability = {
387
521
  export const completeArtifactFileCapability = {
388
522
  key: 'artifact_complete_file',
389
523
  names: { sdk: 'artifact_complete_file', mcp: 'ziggs_artifact_complete_file' },
390
- title: 'Finish a file upload',
524
+ title: 'Finish a large file upload',
391
525
  descriptions: {
392
- sdk: 'Finish a file upload after the S3 PUT. Pass artifactId + sha256 hex of the bytes ' +
393
- 'you uploaded. Sets extractionStatus=pending and enqueues extract. Poll artifact_list ' +
394
- 'or context until extractionStatus is ok/failed; download still works while pending.',
395
- mcp: 'Finish a file upload after the S3 PUT (or after ziggs_artifact_upload_url returned ' +
396
- 'checksum). Pass artifactId + sha256 hex. Enqueues text extract. When extractionStatus ' +
397
- 'is ok, read extracted text via ziggs_context_read / grants — file bytes stay on ' +
398
- 'ziggs_artifact_download.',
526
+ sdk: 'Second half of the large-file escape hatch — only needed after artifact_upload_url. ' +
527
+ 'Pass artifactId + sha256 hex of the bytes you PUT. Sets extractionStatus=pending and ' +
528
+ 'enqueues extract. Poll artifact_list or context until extractionStatus is ok/failed; ' +
529
+ 'download still works while pending.',
530
+ mcp: 'Second half of the large-file escape hatch — only needed after ziggs_artifact_upload_url. ' +
531
+ 'Pass artifactId + sha256 hex of the bytes you PUT. Enqueues text extract. When ' +
532
+ 'extractionStatus is ok, read extracted text via ziggs_context_read / grants — file bytes ' +
533
+ 'stay on ziggs_artifact_download.',
399
534
  },
400
535
  annotation: 'write',
401
536
  params: {
@@ -1,9 +1,8 @@
1
1
  import { type CapabilityDefinition } from './types.js';
2
2
  /**
3
3
  * the decided chat surface for agents: chat_open only.
4
- * There is deliberately NO chat-listing tool on the SDK: agents read context
5
- * only via held grants and chat membership auto-mints a chat grant,
6
- * so "what chats can I see" is already answered by grant_list
4
+ * There is deliberately NO chat-listing tool on the SDK: taking part in a room
5
+ * IS a grant on it, so "what chats can I see" is already answered by grant_list
7
6
  * scopeKind=chat. MCP keeps its rich session-UX lister
8
7
  * (ziggs_chat_list) for humans in Cursor/Claude.
9
8
  */
@@ -2,9 +2,8 @@ import { openConversation } from '../http/ChatClient.js';
2
2
  import { fullCreds } from './types.js';
3
3
  /**
4
4
  * the decided chat surface for agents: chat_open only.
5
- * There is deliberately NO chat-listing tool on the SDK: agents read context
6
- * only via held grants and chat membership auto-mints a chat grant,
7
- * so "what chats can I see" is already answered by grant_list
5
+ * There is deliberately NO chat-listing tool on the SDK: taking part in a room
6
+ * IS a grant on it, so "what chats can I see" is already answered by grant_list
8
7
  * scopeKind=chat. MCP keeps its rich session-UX lister
9
8
  * (ziggs_chat_list) for humans in Cursor/Claude.
10
9
  */
@@ -13,8 +12,8 @@ export const openConversationCapability = {
13
12
  names: { sdk: 'chat_open', mcp: 'ziggs_chat_open' },
14
13
  title: 'Open a chat',
15
14
  descriptions: {
16
- sdk: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat, so it is how you find the conversation you already have with someone — pass newChat only when this really is a separate subject. To reach an agent in ANOTHER org, establish a link first — link_propose (if you have its agent id) or link_create_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED. To list chats you can already read, use grant_list scopeKind=chat.',
17
- mcp: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat; pass newChat only when this really is a separate subject. To reach an agent in ANOTHER org, an unpublished agent must establish a link first — ziggs_link_propose (if you have its agent id) or ziggs_link_create_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED.',
15
+ sdk: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat, so it is how you find the conversation you already have with someone — pass newChat only when this really is a separate subject. This is also how you reach the HUMAN who hired you when the work needs an answer only they have: pass their user id (context_snapshot lists it under users; on your agreement they are the payer/creator party), then chat_send in the chat it returns with no receiverId. Opening a room is issuing the other side access to it, so it needs something that authorizes that: they publish a listing, you share an org, you hold a live agreement with them (a link counts, so link_propose is how you get one with a peer in another org), or they claimed your invite. With none of those the call is refused and the refusal names the levers. To list chats you can already read, use grant_list scopeKind=chat.',
16
+ mcp: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat; pass newChat only when this really is a separate subject. Opening a room is issuing the other side access to it, so it needs something that authorizes that: they publish a listing, you share an org, you hold a live agreement with them (a link counts, so ziggs_link_propose is how you get one with a peer in another org), or they claimed your invite. With none of those the call is refused and names the levers.',
18
17
  },
19
18
  annotation: 'write',
20
19
  params: {
@@ -38,7 +37,7 @@ export const openConversationCapability = {
38
37
  throw new Error('participantId is required');
39
38
  const { chatId, reused } = await openConversation(args['participantId'], fullCreds(env), { newChat: args['newChat'] === true });
40
39
  const lister = env.surface === 'mcp' ? 'ziggs_grant_list' : 'grant_list';
41
- const grantNote = `Your membership auto-mints a chat grant, so the chat also appears in ${lister} scopeKind=chat for context reads.`;
40
+ const grantNote = `Opening the room issued you a grant on it, so the chat also appears in ${lister} scopeKind=chat for context reads.`;
42
41
  // word the note from the outcome instead of covering both cases.
43
42
  // "Open (or reused)" told the agent the distinction existed and then
44
43
  // withheld it — worse than silence, because the agent cannot even tell