@ziggs-ai/api-client 0.10.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/capabilities/agreementVerbs.d.ts +2 -2
  2. package/dist/capabilities/agreementVerbs.js +19 -19
  3. package/dist/capabilities/agreements.d.ts +1 -1
  4. package/dist/capabilities/agreements.js +5 -5
  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 +2 -2
  14. package/dist/capabilities/index.js +2 -2
  15. package/dist/capabilities/links.d.ts +1 -1
  16. package/dist/capabilities/links.js +5 -7
  17. package/dist/capabilities/marketplace.js +20 -14
  18. package/dist/capabilities/payments.d.ts +24 -8
  19. package/dist/capabilities/payments.js +28 -392
  20. package/dist/capabilities/proposeProviderId.d.ts +1 -1
  21. package/dist/capabilities/proposeProviderId.js +1 -1
  22. package/dist/http/AgreementClient.d.ts +24 -19
  23. package/dist/http/AgreementClient.js +22 -14
  24. package/dist/http/ChatClient.d.ts +1 -0
  25. package/dist/http/ChatClient.js +4 -1
  26. package/dist/http/ConnectionsClient.js +12 -1
  27. package/dist/http/ContextGrantsClient.d.ts +15 -1
  28. package/dist/http/ContextGrantsClient.js +2 -0
  29. package/dist/http/ContextReadClient.d.ts +14 -5
  30. package/dist/http/GrantsClient.d.ts +14 -0
  31. package/dist/http/GrantsClient.js +18 -2
  32. package/dist/http/MarketplaceClient.d.ts +6 -6
  33. package/dist/http/MarketplaceClient.js +11 -11
  34. package/dist/http/TaskClient.d.ts +5 -0
  35. package/dist/http/TaskClient.js +2 -0
  36. package/dist/http/agreementFlows.d.ts +4 -4
  37. package/dist/http/agreementFlows.js +9 -10
  38. package/dist/http/grants.d.ts +28 -0
  39. package/dist/http/index.d.ts +2 -2
  40. package/dist/index.d.ts +1 -1
  41. package/dist/types.d.ts +44 -15
  42. package/package.json +1 -1
@@ -1,8 +1,8 @@
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
6
  export declare const agreementOfferCapability: CapabilityDefinition;
7
7
  export declare const agreementHandoffCapability: CapabilityDefinition;
8
8
  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',
@@ -101,13 +101,13 @@ function publishedNext(env) {
101
101
  ];
102
102
  }
103
103
  /* ─────────────────────────── 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',
104
+ export const agreementBuyCapability = {
105
+ key: 'agreement_buy',
106
+ names: { sdk: 'agreement_buy', mcp: 'ziggs_agreement_buy' },
107
+ title: 'They work, you pay (named counterparty)',
108
108
  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.',
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_request.',
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_request.',
111
111
  },
112
112
  annotation: 'write',
113
113
  params: {
@@ -133,10 +133,10 @@ export const agreementCommissionCapability = {
133
133
  export const agreementBidCapability = {
134
134
  key: 'agreement_bid',
135
135
  names: { sdk: 'agreement_bid', mcp: 'ziggs_agreement_bid' },
136
- title: 'Offer to work for a counterparty',
136
+ title: 'You work, they pay (named counterparty)',
137
137
  descriptions: {
138
138
  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.',
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_buy.',
140
140
  },
141
141
  annotation: 'write',
142
142
  params: {
@@ -162,7 +162,7 @@ export const agreementBidCapability = {
162
162
  export const agreementBrokerCapability = {
163
163
  key: 'agreement_broker',
164
164
  names: { sdk: 'agreement_broker', mcp: 'ziggs_agreement_broker' },
165
- title: 'Broker work between two other parties',
165
+ title: 'A third party does the work, you arrange it',
166
166
  descriptions: {
167
167
  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
168
  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.',
@@ -195,10 +195,10 @@ export const agreementBrokerCapability = {
195
195
  sdkOptions: { isAgreementCreation: true },
196
196
  };
197
197
  /* ──────────────────────────────── broadcast ──────────────────────────────── */
198
- export const agreementQuestCapability = {
199
- key: 'agreement_quest',
200
- names: { sdk: 'agreement_quest', mcp: 'ziggs_agreement_quest' },
201
- title: 'Post a quest',
198
+ export const agreementRequestCapability = {
199
+ key: 'agreement_request',
200
+ names: { sdk: 'agreement_request', mcp: 'ziggs_agreement_request' },
201
+ title: 'They work, you pay (open to anyone)',
202
202
  descriptions: {
203
203
  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
204
  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.',
@@ -220,10 +220,10 @@ export const agreementQuestCapability = {
220
220
  export const agreementOfferCapability = {
221
221
  key: 'agreement_offer',
222
222
  names: { sdk: 'agreement_offer', mcp: 'ziggs_agreement_offer' },
223
- title: 'Publish a standing offer',
223
+ title: 'You work, they pay (open to anyone)',
224
224
  descriptions: {
225
225
  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.',
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_request.',
227
227
  },
228
228
  annotation: 'write',
229
229
  params: { audience: AUDIENCE_PARAM, ...TERMS_PARAMS },
@@ -248,7 +248,7 @@ export const agreementOfferCapability = {
248
248
  export const agreementHandoffCapability = {
249
249
  key: 'agreement_handoff',
250
250
  names: { sdk: 'agreement_handoff', mcp: 'ziggs_agreement_handoff' },
251
- title: 'Hand off an agent you hired',
251
+ title: 'Pass a hire you hold to whoever claims it',
252
252
  descriptions: {
253
253
  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
254
  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.',
@@ -296,10 +296,10 @@ export const agreementHandoffCapability = {
296
296
  sdkOptions: { isAgreementCreation: true },
297
297
  };
298
298
  export const AGREEMENT_VERB_CAPABILITIES = [
299
- agreementCommissionCapability,
299
+ agreementBuyCapability,
300
300
  agreementBidCapability,
301
301
  agreementBrokerCapability,
302
- agreementQuestCapability,
302
+ agreementRequestCapability,
303
303
  agreementOfferCapability,
304
304
  agreementHandoffCapability,
305
305
  ];
@@ -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,15 +11,15 @@ 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
23
  },
24
24
  },
25
25
  needsAgentId: true,
@@ -40,7 +40,7 @@ export const agreementClaimCapability = {
40
40
  ? 'Standing offer claimed — the publisher provides, your side pays. Spawn work under it with the task-create tool.'
41
41
  : kind === 'hand-off'
42
42
  ? '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.',
43
+ : 'Request claimed — you provide the work. Read the terms, then post progress and set the task result under this agreement.',
44
44
  agreement,
45
45
  };
46
46
  },
@@ -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
@@ -63,7 +63,7 @@ export const requestConnectionCapability = {
63
63
  'On approval the server is connected (browser OAuth if needed) and you are granted the tools; call them with mcp_tool_call / mcp_tools_list (not connection_proxy).',
64
64
  mcp: 'Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. ' +
65
65
  'Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement (there is no MCP tool to approve it, so tell them to approve it in the chat). ' +
66
- '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 (not ziggs_connection_proxy).',
66
+ '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.',
67
67
  },
68
68
  annotation: 'write',
69
69
  params: {
@@ -214,6 +214,9 @@ export const contextDelegateCapability = {
214
214
  const client = new ContextGrantsClient(creds.operatorKey, creds.agentId);
215
215
  const result = await client.delegateGrant(args['parentGrantId'], {
216
216
  holderId: args['holderId'],
217
+ // An agent passing part of its own grant onward hands it to another
218
+ // agent; a person is granted through their own surface.
219
+ holderKind: 'agent',
217
220
  scope: { kind: scopeKind, id: scopeId },
218
221
  temporal: temporal,
219
222
  expiresAt: args['expiresAt'] ?? undefined,
@@ -5,12 +5,13 @@ import { type CapabilityDefinition } from './types.js';
5
5
  * cross-session. `unreadableRails` comes from the backend so a short
6
6
  * list is never presented as complete when the key can't read a rail.
7
7
  *
8
- * HOLD, not reach. `GET /grants` lists granted rows and nothing else; access
9
- * that is implicit rather than granted (authorship, chat membership, agreement
10
- * party, org membership) leaves no row behind, so a reader can be entitled to
11
- * something this list will never mention. The description
12
- * says so, because the old "the single answer" wording was read as completeness
13
- * and an empty list as "no access".
8
+ * HOLD, not reach. `GET /grants` lists rows this holder is named on, and taking
9
+ * part in a room or being party to a live agreement now IS such a row. Two kinds
10
+ * of access still leave none: authoring an artifact, and the org memberships
11
+ * that make an ORG-held grant cover you (the row names the org, not you). So a
12
+ * reader can still be entitled to something this list will never mention, and
13
+ * the description says so, because the old "the single answer" wording was read
14
+ * as completeness and an empty list as "no access".
14
15
  */
15
16
  export declare const listGrantsCapability: CapabilityDefinition;
16
17
  export declare const GRANTS_CAPABILITIES: CapabilityDefinition[];
@@ -27,20 +27,21 @@ function parseScopeKinds(raw) {
27
27
  * cross-session. `unreadableRails` comes from the backend so a short
28
28
  * list is never presented as complete when the key can't read a rail.
29
29
  *
30
- * HOLD, not reach. `GET /grants` lists granted rows and nothing else; access
31
- * that is implicit rather than granted (authorship, chat membership, agreement
32
- * party, org membership) leaves no row behind, so a reader can be entitled to
33
- * something this list will never mention. The description
34
- * says so, because the old "the single answer" wording was read as completeness
35
- * and an empty list as "no access".
30
+ * HOLD, not reach. `GET /grants` lists rows this holder is named on, and taking
31
+ * part in a room or being party to a live agreement now IS such a row. Two kinds
32
+ * of access still leave none: authoring an artifact, and the org memberships
33
+ * that make an ORG-held grant cover you (the row names the org, not you). So a
34
+ * reader can still be entitled to something this list will never mention, and
35
+ * the description says so, because the old "the single answer" wording was read
36
+ * as completeness and an empty list as "no access".
36
37
  */
37
38
  export const listGrantsCapability = {
38
39
  key: 'grant_list',
39
40
  names: { sdk: 'grant_list', mcp: 'ziggs_grant_list' },
40
41
  title: 'List grants you hold',
41
42
  descriptions: {
42
- sdk: 'List grants this agent holds — or, with role=issuer, grants this agent caused (author-shares and delegate children). Context (chat/agreement/org/artifact), connection, and wallet — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). Default role=holder answers "what grants do I HOLD?", which is narrower than "what can I reach?": authorship, chat membership, agreement party, and org membership leave no grant row — an empty holder list means "no grants", never "no access". role=issuer answers "who did I share with / what did I mint?" after artifact_share. Filter by scopeKind, scopeId, and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails. Cursor-paginated.',
43
- mcp: 'List grants this delegate holds — or, with role=issuer, grants this delegate caused (author-shares and delegate children). Context (chat/agreement/org/artifact), connection, and wallet — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). Default role=holder answers "what grants do I HOLD?", which is narrower than "what can I reach?": authorship, chat membership, agreement party, and org membership leave no grant row — an empty holder list means "no grants", never "no access". After ziggs_artifact_share, pass role=issuer (and usually scopeKind=["artifact"]) to recover grantIds and revoke with ziggs_context_revoke_grant. Filter by scopeKind, scopeId, and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails. Cursor-paginated.',
43
+ sdk: 'List grants this agent holds — or, with role=issuer, grants this agent caused (author-shares and delegate children). Context (chat/agreement/org/artifact), connection, and wallet — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). Default role=holder answers "what grants do I HOLD?", which is narrower than "what can I reach?": your own authorship, and grants held by an org you belong to rather than by you, leave nothing under your own id — an empty holder list means "no grants of your own", never "no access". role=issuer answers "who did I share with / what did I mint?" after artifact_share. Filter by scopeKind, scopeId, and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails. Cursor-paginated.',
44
+ mcp: 'List grants this delegate holds — or, with role=issuer, grants this delegate caused (author-shares and delegate children). Context (chat/agreement/org/artifact), connection, and wallet — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). Default role=holder answers "what grants do I HOLD?", which is narrower than "what can I reach?": your own authorship, and grants held by an org you belong to rather than by you, leave nothing under your own id — an empty holder list means "no grants of your own", never "no access". After ziggs_artifact_share, pass role=issuer (and usually scopeKind=["artifact"]) to recover grantIds and revoke with ziggs_context_revoke_grant. Filter by scopeKind, scopeId, and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails. Cursor-paginated.',
44
45
  },
45
46
  annotation: 'read-only',
46
47
  params: {
@@ -1,7 +1,7 @@
1
1
  export { type CapabilitySurface, type CapabilityAnnotation, type CapabilityParam, type CapabilityEnv, type CapabilityDefinition, fullCreds, rethrowWithContext, } from './types.js';
2
2
  export { nextCall, peerAgentId, type NextCall } from './nextCall.js';
3
- export { AGREEMENT_VERB_CAPABILITIES, agreementCommissionCapability, agreementBidCapability, agreementBrokerCapability, agreementQuestCapability, agreementOfferCapability, agreementHandoffCapability, } from './agreementVerbs.js';
4
- export { PAYMENT_CAPABILITIES, paymentBalanceCapability, paymentTransferCapability, paymentWaitForApprovalCapability, paymentHoldCapability, paymentReleaseCapability, paymentResolveWalletCapability, paymentIssueGrantCapability, paymentAttenuateGrantCapability, paymentRevokeGrantCapability, } from './payments.js';
3
+ export { AGREEMENT_VERB_CAPABILITIES, agreementBuyCapability, agreementBidCapability, agreementBrokerCapability, agreementRequestCapability, agreementOfferCapability, agreementHandoffCapability, } from './agreementVerbs.js';
4
+ export { PAYMENT_CAPABILITIES, paymentBalanceCapability } from './payments.js';
5
5
  export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability, proposeLinkCapability, linkIsReachOnly, } from './links.js';
6
6
  export { AGREEMENT_CAPABILITIES, agreementClaimCapability } from './agreements.js';
7
7
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
@@ -1,7 +1,7 @@
1
1
  export { fullCreds, rethrowWithContext, } from './types.js';
2
2
  export { nextCall, peerAgentId } from './nextCall.js';
3
- export { AGREEMENT_VERB_CAPABILITIES, agreementCommissionCapability, agreementBidCapability, agreementBrokerCapability, agreementQuestCapability, agreementOfferCapability, agreementHandoffCapability, } from './agreementVerbs.js';
4
- export { PAYMENT_CAPABILITIES, paymentBalanceCapability, paymentTransferCapability, paymentWaitForApprovalCapability, paymentHoldCapability, paymentReleaseCapability, paymentResolveWalletCapability, paymentIssueGrantCapability, paymentAttenuateGrantCapability, paymentRevokeGrantCapability, } from './payments.js';
3
+ export { AGREEMENT_VERB_CAPABILITIES, agreementBuyCapability, agreementBidCapability, agreementBrokerCapability, agreementRequestCapability, agreementOfferCapability, agreementHandoffCapability, } from './agreementVerbs.js';
4
+ export { PAYMENT_CAPABILITIES, paymentBalanceCapability } from './payments.js';
5
5
  export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability, proposeLinkCapability, linkIsReachOnly, } from './links.js';
6
6
  export { AGREEMENT_CAPABILITIES, agreementClaimCapability } from './agreements.js';
7
7
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';