@ziggs-ai/api-client 0.40.4 → 0.40.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.40.5
6
+
7
+ - An artifact reaches people only by being added to a bucket. The `artifact_share` tool, `shareArtifactCapability` and `ContextGrantsClient.shareArtifact` are removed, `context_delegate` no longer takes `scopeKind: 'artifact'`, and `link_list` no longer suggests sharing an artifact. The server refuses artifact grants issued or passed on directly.
8
+ - Buckets. A bucket is pointers to items (messages, artifacts, tasks, agreements) and to who it is for; it holds no permission, and whether everyone in it can read each item is computed from the items' own grants when asked. Every message is sent through a bucket, and the bucket points at it; no message names its bucket. `BucketsClient` covers the operations: `listMine(item)` finds the buckets holding an item, the first being where a message was sent; `contents` reads what is in a bucket as you can read it now, under the selected run; `sendMessage` sends inside a bucket to its people, under the selected run; `addMembers` takes `onMisfit: 'share' | 'split'`, where split leaves the bucket as it is and starts a narrower one with the newcomers and only what everyone can read; `join` brings another bucket's people and items in. The bucket tools are `bucket_create`, `bucket_list`, `bucket_read`, `bucket_insert`, `bucket_remove`, `bucket_duplicate`, `bucket_move` (into another bucket, or a new one when given a name), `bucket_add_members`, `bucket_join`, `bucket_send`, `bucket_remove_member`, `bucket_rename` and `bucket_delete`.
9
+ - A refusal's `impact` is carried through `describeOperationError`, next to `choices` and `recovery`. A bucket add or join that someone cannot read everything in now lists the misfits (`impact.misfits`, with any shares made in `impact.shared`) and the onMisfit values left (`choices`); a reply that leaves someone in its bucket out names the bucket and who is missing (`impact.bucketId`, `impact.missing`).
10
+
5
11
  ## 0.40.4
6
12
 
7
13
  - `InboxEnvelope.bellAt`: the latest delivery a read answered. `getInbox({ seen })` passes it back so a held read answers only mail delivered after it.
@@ -46,17 +46,6 @@ export interface UnsearchedContext {
46
46
  * scopes that were never opened. `coverage` sits between them naming what ran.
47
47
  */
48
48
  export declare const findArtifactsCapability: CapabilityDefinition;
49
- /**
50
- * hand one artifact you authored to one other agent.
51
- *
52
- * Deliberately not part of `context_delegate`: there is no parent grant to
53
- * attenuate here. An agent holds no grant over its own output — authorship is
54
- * the authority, and it belongs to the agent's owner, which is why a receiver
55
- * outside that owner's trust boundary needs their approval. What counts as
56
- * "inside" is the shared new-party policy, not a local rule: same owner, same
57
- * org, or an active agent link.
58
- */
59
- export declare const shareArtifactCapability: CapabilityDefinition;
60
49
  /**
61
50
  * attach an artifact you already have to a chat, a task, or an agreement.
62
51
  *
@@ -64,7 +53,7 @@ export declare const shareArtifactCapability: CapabilityDefinition;
64
53
  * by itself: it puts the artifact inside the container, and that container's
65
54
  * audience can read it from then on. Use this to publish to a room, bind a
66
55
  * deliverable to a task, or restore agreement scope after a free-standing
67
- * record; use artifact_share to hand it to ONE agent instead.
56
+ * record. To give it to someone, add it to a bucket with them.
68
57
  */
69
58
  export declare const attachArtifactCapability: CapabilityDefinition;
70
59
  /**
@@ -1,6 +1,5 @@
1
1
  import { SOURCES_PARAM, sourcesArg } from './sources.js';
2
2
  import { ARTIFACT_FILE_FORMATS, ArtifactsClient, } from '../http/ArtifactsClient.js';
3
- import { ContextGrantsClient } from '../http/ContextGrantsClient.js';
4
3
  import { ContextDiscoveryClient, } from '../http/ContextDiscoveryClient.js';
5
4
  import { fullCreds } from './types.js';
6
5
  import { LIST_FIELDS_PARAM, listedFieldNote, parseListFields, pickListedRows } from './listedFields.js';
@@ -328,18 +327,6 @@ const OPEN_ID_FIELD = {
328
327
  artifact: 'artifactId',
329
328
  task: 'taskId',
330
329
  };
331
- function pendingAccessReceipt(result, extra) {
332
- return {
333
- ...extra,
334
- ok: false,
335
- outcome: 'pending',
336
- status: 'pending_approval',
337
- state: 'requested',
338
- recoverable: true,
339
- agreementId: result.agreementId,
340
- ownerId: result.ownerId,
341
- };
342
- }
343
330
  /**
344
331
  * The locked scopes near a search that found nothing.
345
332
  *
@@ -449,73 +436,6 @@ export const findArtifactsCapability = {
449
436
  return { ...shaped, notSearched: await unsearchedContext(env) };
450
437
  },
451
438
  };
452
- /**
453
- * hand one artifact you authored to one other agent.
454
- *
455
- * Deliberately not part of `context_delegate`: there is no parent grant to
456
- * attenuate here. An agent holds no grant over its own output — authorship is
457
- * the authority, and it belongs to the agent's owner, which is why a receiver
458
- * outside that owner's trust boundary needs their approval. What counts as
459
- * "inside" is the shared new-party policy, not a local rule: same owner, same
460
- * org, or an active agent link.
461
- */
462
- export const shareArtifactCapability = {
463
- key: 'artifact_share',
464
- names: { sdk: 'artifact_share', mcp: 'ziggs_artifact_share' },
465
- title: 'Share an artifact',
466
- descriptions: {
467
- sdk: 'Share an artifact you authored with another agent — just that artifact, no chat or agreement. An agent already connected to you (same owner, same org, or an active link) gets it immediately; anyone else is offered to your owner to approve.',
468
- },
469
- annotation: 'destructive',
470
- params: {
471
- artifactId: {
472
- type: 'string',
473
- required: true,
474
- description: 'The artifact to share — you must be its author',
475
- },
476
- holderId: { type: 'string', required: true, description: 'Agent id that receives it' },
477
- expiresAt: {
478
- type: 'string',
479
- description: 'ISO-8601 expiry; omit for the default grant lifetime',
480
- },
481
- idempotencyKey: {
482
- type: 'string',
483
- description: 'Replay the same share. Same key and payload recover the original request; the same key with a different payload is a conflict, not a second share.',
484
- },
485
- },
486
- needsAgentId: true,
487
- handler: async (args, env) => {
488
- if ('addressId' in args)
489
- throw new Error('addressId is retired; use explicit work or an exact message artifact reference');
490
- const creds = fullCreds(env);
491
- const artifactId = args['artifactId'];
492
- if (!artifactId)
493
- throw new Error('artifactId is required');
494
- const holderId = args['holderId'];
495
- if (!holderId)
496
- throw new Error('holderId is required');
497
- const result = await new ContextGrantsClient(creds.operatorKey, creds.agentId, env.baseUrl).shareArtifact(artifactId, {
498
- holderId,
499
- expiresAt: args['expiresAt'],
500
- }, {
501
- idempotencyKey: typeof args['idempotencyKey'] === 'string'
502
- ? args['idempotencyKey']
503
- : undefined,
504
- });
505
- if (result.status === 'pending_approval') {
506
- return pendingAccessReceipt(result, {
507
- ...result,
508
- note: `Requested, not granted — ${result.ownerId ?? 'the owner'} must approve agreement ${result.agreementId}. Recover that id; do not open a second card.`,
509
- });
510
- }
511
- return {
512
- ok: true,
513
- outcome: 'ok',
514
- ...result,
515
- note: `${holderId} can now read artifact ${artifactId} with via=artifact:${artifactId}.`,
516
- };
517
- },
518
- };
519
439
  /**
520
440
  * attach an artifact you already have to a chat, a task, or an agreement.
521
441
  *
@@ -523,14 +443,14 @@ export const shareArtifactCapability = {
523
443
  * by itself: it puts the artifact inside the container, and that container's
524
444
  * audience can read it from then on. Use this to publish to a room, bind a
525
445
  * deliverable to a task, or restore agreement scope after a free-standing
526
- * record; use artifact_share to hand it to ONE agent instead.
446
+ * record. To give it to someone, add it to a bucket with them.
527
447
  */
528
448
  export const attachArtifactCapability = {
529
449
  key: 'artifact_attach',
530
450
  names: { sdk: 'artifact_attach', mcp: 'ziggs_artifact_attach' },
531
451
  title: 'Attach to a task or agreement',
532
452
  descriptions: {
533
- sdk: 'Attach an artifact you can read to a task or agreement. To reference it in a message use message_send with an exact artifact reference; that reference grants no access. Task/agreement links remain subject to their work and source-disclosure policies. Use artifact_share to give it to one specific agent instead.',
453
+ sdk: 'Attach an artifact you can read to a task or agreement. To reference it in a message use message_send with an exact artifact reference; that reference grants no access. Task/agreement links remain subject to their work and source-disclosure policies. To give it to someone, add it to a bucket with them.',
534
454
  },
535
455
  annotation: 'destructive',
536
456
  params: {
@@ -818,7 +738,6 @@ export const ARTIFACT_CAPABILITIES = [
818
738
  recordArtifactCapability,
819
739
  listArtifactsCapability,
820
740
  findArtifactsCapability,
821
- shareArtifactCapability,
822
741
  attachArtifactCapability,
823
742
  reviewArtifactContributionCapability,
824
743
  contributeArtifactCapability,
@@ -0,0 +1,15 @@
1
+ import { type CapabilityDefinition } from './types.js';
2
+ export declare const bucketCreateCapability: CapabilityDefinition;
3
+ export declare const bucketListCapability: CapabilityDefinition;
4
+ export declare const bucketReadCapability: CapabilityDefinition;
5
+ export declare const bucketInsertCapability: CapabilityDefinition;
6
+ export declare const bucketMoveCapability: CapabilityDefinition;
7
+ export declare const bucketRemoveCapability: CapabilityDefinition;
8
+ export declare const bucketDuplicateCapability: CapabilityDefinition;
9
+ export declare const bucketAddMembersCapability: CapabilityDefinition;
10
+ export declare const bucketJoinCapability: CapabilityDefinition;
11
+ export declare const bucketSendCapability: CapabilityDefinition;
12
+ export declare const bucketRemoveMemberCapability: CapabilityDefinition;
13
+ export declare const bucketRenameCapability: CapabilityDefinition;
14
+ export declare const bucketDeleteCapability: CapabilityDefinition;
15
+ export declare const BUCKET_CAPABILITIES: CapabilityDefinition[];
@@ -0,0 +1,353 @@
1
+ import { BucketsClient, BUCKET_ITEM_KINDS, } from '../http/BucketsClient.js';
2
+ import { fullCreds, rethrowWithContext } from './types.js';
3
+ /**
4
+ * The bucket operations as tools, one per route.
5
+ *
6
+ * A bucket is a high-level way to work with context: pointers to items and to
7
+ * who it is for, with no permission of its own. Whether everyone it is for can
8
+ * read each item is computed from the items' own grants whenever it is asked;
9
+ * what does not fit comes back as `misfits`, and the caller chooses to share
10
+ * or to split. Every message is sent through a bucket.
11
+ */
12
+ const BUCKET_ID = {
13
+ type: 'string',
14
+ required: true,
15
+ };
16
+ const ITEMS = {
17
+ type: 'array',
18
+ required: true,
19
+ description: '{kind, id}, up to 100.',
20
+ items: {
21
+ type: 'object',
22
+ properties: {
23
+ kind: { type: 'string', enum: [...BUCKET_ITEM_KINDS] },
24
+ id: { type: 'string' },
25
+ },
26
+ required: ['kind', 'id'],
27
+ },
28
+ };
29
+ const SHARE = {
30
+ type: 'boolean',
31
+ description: 'Share misfits first.',
32
+ };
33
+ const AUDIENCE = {
34
+ type: 'array',
35
+ description: 'Other person, agent or workspace ids.',
36
+ items: { type: 'string' },
37
+ };
38
+ function client(env) {
39
+ const creds = fullCreds(env);
40
+ return new BucketsClient(creds.operatorKey, creds.agentId || undefined);
41
+ }
42
+ function bucketId(args) {
43
+ const id = args['bucketId'];
44
+ if (typeof id !== 'string' || !id.trim())
45
+ throw new Error('bucketId is required');
46
+ return id.trim();
47
+ }
48
+ function items(args) {
49
+ const raw = args['items'];
50
+ if (!Array.isArray(raw) || raw.length === 0)
51
+ throw new Error('items must be a non-empty list of {kind, id}');
52
+ return raw.map((entry) => {
53
+ const item = entry;
54
+ if (!BUCKET_ITEM_KINDS.includes(item?.kind) || typeof item.id !== 'string' || !item.id.trim()) {
55
+ throw new Error(`Each item is {kind, id} with kind one of ${BUCKET_ITEM_KINDS.join(', ')}`);
56
+ }
57
+ return { kind: item.kind, id: item.id.trim() };
58
+ });
59
+ }
60
+ function principals(raw, field) {
61
+ if (raw === undefined)
62
+ return undefined;
63
+ if (!Array.isArray(raw) || raw.some((id) => typeof id !== 'string' || !id.trim())) {
64
+ throw new Error(`${field} must be a list of ids`);
65
+ }
66
+ return raw.map((id) => id.trim());
67
+ }
68
+ async function run(label, work) {
69
+ try {
70
+ return await work();
71
+ }
72
+ catch (e) {
73
+ rethrowWithContext(e, label);
74
+ }
75
+ }
76
+ export const bucketCreateCapability = {
77
+ key: 'bucket_create',
78
+ names: { sdk: 'bucket_create', mcp: 'ziggs_bucket_create' },
79
+ title: 'Create a bucket',
80
+ descriptions: {
81
+ sdk: 'Create a bucket: a working set whose whole audience can read every item in it. You are always in it. Items given here go in exactly as bucket_insert puts them.',
82
+ },
83
+ annotation: 'write',
84
+ needsAgentId: true,
85
+ params: {
86
+ name: { type: 'string', required: true },
87
+ audience: AUDIENCE,
88
+ items: { ...ITEMS, required: false },
89
+ share: SHARE,
90
+ },
91
+ handler: (args, env) => run('bucket_create', () => client(env).create({
92
+ name: String(args['name'] ?? ''),
93
+ ...(args['audience'] !== undefined ? { audience: principals(args['audience'], 'audience') } : {}),
94
+ ...(args['items'] !== undefined ? { items: items(args) } : {}),
95
+ ...(args['share'] === true ? { share: true } : {}),
96
+ })),
97
+ };
98
+ export const bucketListCapability = {
99
+ key: 'bucket_list',
100
+ names: { sdk: 'bucket_list', mcp: 'ziggs_bucket_list' },
101
+ title: 'List buckets',
102
+ descriptions: { sdk: 'List the buckets you are in: name, audience, revision and item count.' },
103
+ annotation: 'read-only',
104
+ needsAgentId: true,
105
+ params: {},
106
+ handler: (_args, env) => run('bucket_list', () => client(env).listMine()),
107
+ };
108
+ export const bucketReadCapability = {
109
+ key: 'bucket_read',
110
+ names: { sdk: 'bucket_read', mcp: 'ziggs_bucket_read' },
111
+ title: 'Read a bucket',
112
+ descriptions: {
113
+ sdk: 'Read one bucket and a page of its items, newest first. Items are references; read each one with its own tool (message_read, open). Pass nextCursor as before for the next page.',
114
+ },
115
+ annotation: 'read-only',
116
+ needsAgentId: true,
117
+ params: {
118
+ bucketId: BUCKET_ID,
119
+ before: { type: 'string', description: 'Cursor from the previous page.' },
120
+ limit: { type: 'number' },
121
+ },
122
+ handler: (args, env) => run('bucket_read', async () => {
123
+ const id = bucketId(args);
124
+ const c = client(env);
125
+ const [bucket, page] = await Promise.all([
126
+ c.get(id),
127
+ c.items(id, {
128
+ ...(typeof args['before'] === 'string' ? { before: args['before'] } : {}),
129
+ ...(typeof args['limit'] === 'number' ? { limit: args['limit'] } : {}),
130
+ }),
131
+ ]);
132
+ return { bucket, ...page };
133
+ }),
134
+ };
135
+ export const bucketInsertCapability = {
136
+ key: 'bucket_insert',
137
+ names: { sdk: 'bucket_insert', mcp: 'ziggs_bucket_insert' },
138
+ title: 'Put items in a bucket',
139
+ descriptions: {
140
+ sdk: 'Put items in a bucket. Each goes in only if everyone in its audience can read it; the rest come back as misfits, naming who cannot read them when you can read them yourself. With share, an item some members cannot read is first shared with them as you, under the ordinary share rules.',
141
+ },
142
+ annotation: 'write',
143
+ needsAgentId: true,
144
+ params: { bucketId: BUCKET_ID, items: ITEMS, share: SHARE },
145
+ handler: (args, env) => run('bucket_insert', () => client(env).insert(bucketId(args), items(args), args['share'] === true ? { share: true } : {})),
146
+ };
147
+ export const bucketMoveCapability = {
148
+ key: 'bucket_move',
149
+ names: { sdk: 'bucket_move', mcp: 'ziggs_bucket_move' },
150
+ title: 'Move bucket items',
151
+ descriptions: {
152
+ sdk: "Move items out of a bucket into another one (to), or into a new one (name, and audience when not this bucket's): a split. Each moves only if that bucket's audience can all read it; misfits stay where they are.",
153
+ },
154
+ annotation: 'write',
155
+ needsAgentId: true,
156
+ params: {
157
+ bucketId: BUCKET_ID,
158
+ items: ITEMS,
159
+ to: { type: 'string', description: 'An existing bucket.' },
160
+ name: { type: 'string', description: 'A new bucket.' },
161
+ audience: { ...AUDIENCE, description: "With name. Left out: this bucket's." },
162
+ },
163
+ handler: (args, env) => run('bucket_move', () => {
164
+ const to = args['to'];
165
+ const name = args['name'];
166
+ const hasTo = typeof to === 'string' && !!to.trim();
167
+ const hasName = typeof name === 'string' && !!name.trim();
168
+ if (hasTo === hasName)
169
+ throw new Error('Give to (an existing bucket) or name (a new one), not both');
170
+ if (hasTo)
171
+ return client(env).move(bucketId(args), { to: to.trim(), items: items(args) });
172
+ return client(env).split(bucketId(args), {
173
+ items: items(args),
174
+ name: name.trim(),
175
+ ...(args['audience'] !== undefined ? { audience: principals(args['audience'], 'audience') } : {}),
176
+ });
177
+ }),
178
+ };
179
+ export const bucketRemoveCapability = {
180
+ key: 'bucket_remove',
181
+ names: { sdk: 'bucket_remove', mcp: 'ziggs_bucket_remove' },
182
+ title: 'Remove bucket items',
183
+ descriptions: { sdk: 'Take items out of a bucket. The items themselves are untouched; only the reference leaves.' },
184
+ annotation: 'write',
185
+ needsAgentId: true,
186
+ params: { bucketId: BUCKET_ID, items: ITEMS },
187
+ handler: (args, env) => run('bucket_remove', () => client(env).remove(bucketId(args), items(args))),
188
+ };
189
+ export const bucketDuplicateCapability = {
190
+ key: 'bucket_duplicate',
191
+ names: { sdk: 'bucket_duplicate', mcp: 'ziggs_bucket_duplicate' },
192
+ title: 'Duplicate a bucket',
193
+ descriptions: {
194
+ sdk: "Copy items into a new bucket for another audience, for example the part you expose as someone's provider. The original stays whole. Items left out means all of them. Each goes in as bucket_insert puts it.",
195
+ },
196
+ annotation: 'write',
197
+ needsAgentId: true,
198
+ params: {
199
+ bucketId: BUCKET_ID,
200
+ name: { type: 'string', required: true },
201
+ audience: { ...AUDIENCE, description: "Left out: this bucket's." },
202
+ items: { ...ITEMS, required: false, description: 'Left out: all.' },
203
+ share: SHARE,
204
+ },
205
+ handler: (args, env) => run('bucket_duplicate', () => client(env).duplicate(bucketId(args), {
206
+ name: String(args['name'] ?? ''),
207
+ ...(args['audience'] !== undefined ? { audience: principals(args['audience'], 'audience') } : {}),
208
+ ...(args['items'] !== undefined ? { items: items(args) } : {}),
209
+ ...(args['share'] === true ? { share: true } : {}),
210
+ })),
211
+ };
212
+ export const bucketAddMembersCapability = {
213
+ key: 'bucket_add_members',
214
+ names: { sdk: 'bucket_add_members', mcp: 'ziggs_bucket_add_members' },
215
+ title: 'Add to a bucket',
216
+ descriptions: {
217
+ sdk: "Add people, agents or workspaces to a bucket's audience. If one cannot read an item, nothing changes until you choose: onMisfit share gives them access first, under the ordinary share rules; split leaves this bucket as it is and starts a narrower one with them and only what everyone can read.",
218
+ },
219
+ annotation: 'write',
220
+ needsAgentId: true,
221
+ params: {
222
+ bucketId: BUCKET_ID,
223
+ principalIds: { type: 'array', required: true, description: 'Person, agent or workspace ids.', items: { type: 'string' } },
224
+ onMisfit: { type: 'string', enum: ['share', 'split'], description: 'Left out: stop and list misfits.' },
225
+ },
226
+ handler: (args, env) => run('bucket_add_members', () => {
227
+ const choice = args['onMisfit'];
228
+ if (choice !== undefined && choice !== 'share' && choice !== 'split')
229
+ throw new Error('onMisfit is share or split');
230
+ return client(env).addMembers(bucketId(args), principals(args['principalIds'], 'principalIds') ?? [], choice ? { onMisfit: choice } : {});
231
+ }),
232
+ };
233
+ export const bucketJoinCapability = {
234
+ key: 'bucket_join',
235
+ names: { sdk: 'bucket_join', mcp: 'ziggs_bucket_join' },
236
+ title: 'Join buckets',
237
+ descriptions: {
238
+ sdk: "Bring another bucket's people and items into this one. If someone then cannot read an item, nothing changes until you pass onMisfit share. The other bucket stays.",
239
+ },
240
+ annotation: 'write',
241
+ needsAgentId: true,
242
+ params: {
243
+ bucketId: BUCKET_ID,
244
+ from: { type: 'string', required: true },
245
+ onMisfit: { type: 'string', enum: ['share'] },
246
+ },
247
+ handler: (args, env) => run('bucket_join', () => {
248
+ const from = args['from'];
249
+ if (typeof from !== 'string' || !from.trim())
250
+ throw new Error('from is required');
251
+ const choice = args['onMisfit'];
252
+ if (choice !== undefined && choice !== 'share')
253
+ throw new Error('onMisfit is share');
254
+ return client(env).join(bucketId(args), { from: from.trim(), ...(choice ? { onMisfit: 'share' } : {}) });
255
+ }),
256
+ };
257
+ export const bucketSendCapability = {
258
+ key: 'bucket_send',
259
+ names: { sdk: 'bucket_send', mcp: 'ziggs_bucket_send' },
260
+ title: 'Send in a bucket',
261
+ descriptions: {
262
+ sdk: "Send a message inside a bucket. Its readers are the bucket's people; name recipients among them. To reach anyone else, add them with bucket_add_members first. A request wakes the agents it is delivered to.",
263
+ },
264
+ annotation: 'destructive',
265
+ needsAgentId: true,
266
+ params: {
267
+ bucketId: BUCKET_ID,
268
+ text: { type: 'string', required: true },
269
+ recipients: { type: 'array', items: { type: 'string' } },
270
+ intent: { type: 'string', enum: ['fyi', 'request'] },
271
+ replyTo: { type: 'string' },
272
+ operationId: { type: 'string', description: 'Reuse on retry.' },
273
+ },
274
+ handler: (args, env) => run('bucket_send', () => {
275
+ const text = args['text'];
276
+ if (typeof text !== 'string' || !text.trim())
277
+ throw new Error('text is required');
278
+ const intent = args['intent'] ?? 'fyi';
279
+ if (intent !== 'fyi' && intent !== 'request')
280
+ throw new Error('intent is fyi or request');
281
+ const replyTo = args['replyTo'];
282
+ if (replyTo !== undefined && (typeof replyTo !== 'string' || !replyTo.trim()))
283
+ throw new Error('replyTo is a message id');
284
+ const operationId = args['operationId'];
285
+ return client(env).sendMessage(bucketId(args), {
286
+ operationId: typeof operationId === 'string' && operationId.trim() ? operationId.trim() : `bucket-send-${globalThis.crypto.randomUUID()}`,
287
+ content: { text },
288
+ deliveries: { kind: 'explicit', principalIds: principals(args['recipients'], 'recipients') ?? [] },
289
+ intent,
290
+ ...(typeof replyTo === 'string' ? { relationships: { replyToMessageId: replyTo.trim() } } : {}),
291
+ });
292
+ }),
293
+ };
294
+ export const bucketRemoveMemberCapability = {
295
+ key: 'bucket_remove_member',
296
+ names: { sdk: 'bucket_remove_member', mcp: 'ziggs_bucket_remove_member' },
297
+ title: 'Remove from a bucket',
298
+ descriptions: {
299
+ sdk: "Take one person, agent or workspace out of a bucket's audience. The bucket's creator does this, and stays.",
300
+ },
301
+ annotation: 'destructive',
302
+ needsAgentId: true,
303
+ params: {
304
+ bucketId: BUCKET_ID,
305
+ principalId: { type: 'string', required: true },
306
+ },
307
+ handler: (args, env) => run('bucket_remove_member', () => {
308
+ const principalId = args['principalId'];
309
+ if (typeof principalId !== 'string' || !principalId.trim())
310
+ throw new Error('principalId is required');
311
+ return client(env).removeMember(bucketId(args), principalId.trim());
312
+ }),
313
+ };
314
+ export const bucketRenameCapability = {
315
+ key: 'bucket_rename',
316
+ names: { sdk: 'bucket_rename', mcp: 'ziggs_bucket_rename' },
317
+ title: 'Rename a bucket',
318
+ descriptions: { sdk: "Rename a bucket. The bucket's creator does this." },
319
+ annotation: 'write',
320
+ needsAgentId: true,
321
+ params: {
322
+ bucketId: BUCKET_ID,
323
+ name: { type: 'string', required: true },
324
+ },
325
+ handler: (args, env) => run('bucket_rename', () => client(env).rename(bucketId(args), String(args['name'] ?? ''))),
326
+ };
327
+ export const bucketDeleteCapability = {
328
+ key: 'bucket_delete',
329
+ names: { sdk: 'bucket_delete', mcp: 'ziggs_bucket_delete' },
330
+ title: 'Delete a bucket',
331
+ descriptions: {
332
+ sdk: "Delete a bucket. The items themselves are untouched; only the bucket and its pointers go. The bucket's creator does this.",
333
+ },
334
+ annotation: 'destructive',
335
+ needsAgentId: true,
336
+ params: { bucketId: BUCKET_ID },
337
+ handler: (args, env) => run('bucket_delete', () => client(env).delete(bucketId(args))),
338
+ };
339
+ export const BUCKET_CAPABILITIES = [
340
+ bucketCreateCapability,
341
+ bucketListCapability,
342
+ bucketReadCapability,
343
+ bucketInsertCapability,
344
+ bucketRemoveCapability,
345
+ bucketDuplicateCapability,
346
+ bucketMoveCapability,
347
+ bucketAddMembersCapability,
348
+ bucketJoinCapability,
349
+ bucketSendCapability,
350
+ bucketRemoveMemberCapability,
351
+ bucketRenameCapability,
352
+ bucketDeleteCapability,
353
+ ];
@@ -173,18 +173,21 @@ export const contextDiscoverGrantableCapability = {
173
173
  })) };
174
174
  },
175
175
  };
176
+ /** What a grant may be narrowed to when passed on. An artifact is not among
177
+ * them: it reaches people only by being added to a bucket. */
178
+ const DELEGABLE_SCOPE_KINDS = GRANT_SCOPE_KINDS.filter((kind) => kind !== 'artifact');
176
179
  export const contextDelegateCapability = {
177
180
  key: 'context_delegate',
178
181
  names: { sdk: 'context_delegate', mcp: 'ziggs_context_delegate' },
179
182
  title: 'Pass on a narrower grant',
180
183
  descriptions: {
181
- sdk: 'Delegate a narrower child context grant (tighter scope, shorter expiry, or from-now). Holder-only. Delegating a grant whose original owner is another party opens an approval request rather than minting immediately (status: pending_approval).',
184
+ sdk: 'Delegate a narrower child context grant (tighter scope, shorter expiry, or from-now). Holder-only. Delegating a grant whose original owner is another party opens an approval request rather than minting immediately (status: pending_approval). An artifact is not passed on this way: add it to a bucket.',
182
185
  },
183
186
  annotation: 'write',
184
187
  params: {
185
188
  parentGrantId: { type: 'string', required: true },
186
189
  holderId: { type: 'string', required: true, description: 'Agent receiving the delegated grant' },
187
- scopeKind: { type: 'string', required: true, enum: GRANT_SCOPE_KINDS },
190
+ scopeKind: { type: 'string', required: true, enum: DELEGABLE_SCOPE_KINDS },
188
191
  scopeId: { type: 'string', required: true },
189
192
  temporal: {
190
193
  type: 'string',
@@ -210,8 +213,8 @@ export const contextDelegateCapability = {
210
213
  throw new Error('temporal must be from-now or from-start');
211
214
  }
212
215
  const scopeKind = args['scopeKind'];
213
- if (!GRANT_SCOPE_KINDS.includes(scopeKind)) {
214
- throw new Error(`scopeKind must be one of ${GRANT_SCOPE_KINDS.join(', ')}`);
216
+ if (!DELEGABLE_SCOPE_KINDS.includes(scopeKind)) {
217
+ throw new Error(`scopeKind must be one of ${DELEGABLE_SCOPE_KINDS.join(', ')}; an artifact is shared by adding it to a bucket`);
215
218
  }
216
219
  // #6 — an org scope may be named rather than pasted as org_… id.
217
220
  let scopeId = args['scopeId'];
@@ -42,7 +42,7 @@ export const listGrantsCapability = {
42
42
  names: { sdk: 'grant_list', mcp: 'ziggs_grant_list' },
43
43
  title: 'List grants you hold',
44
44
  descriptions: {
45
- sdk: 'List grants this agent holds — or, with role=issuer, grants this agent caused (author-shares and delegate children). Context (chat/agreement/org/artifact/task), connection, wallet, consent, and inbox — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). A `task` scope is a BRANCH of a work graph: the named task and everything under it, which is what holding a pass on a job looks like. 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.',
45
+ sdk: 'List grants this agent holds — or, with role=issuer, grants this agent caused (author-shares and delegate children). Context (chat/agreement/org/artifact/task), connection, wallet, consent, and inbox — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). A `task` scope is a BRANCH of a work graph: the named task and everything under it, which is what holding a pass on a job looks like. 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 a bucket 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.',
46
46
  },
47
47
  annotation: 'read-only',
48
48
  params: {
@@ -13,10 +13,11 @@ export { TASK_CAPABILITIES, listTasksCapability, cancelTaskCapability, delegateT
13
13
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
14
14
  export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
15
15
  export { GRANTS_CAPABILITIES, listGrantsCapability } from './grants.js';
16
+ export { BUCKET_CAPABILITIES, bucketCreateCapability, bucketListCapability, bucketReadCapability, bucketInsertCapability, bucketRemoveCapability, bucketMoveCapability, bucketAddMembersCapability, bucketJoinCapability, bucketSendCapability, bucketRemoveMemberCapability, bucketDuplicateCapability, bucketRenameCapability, bucketDeleteCapability, } from './buckets.js';
16
17
  export { CONTEXT_CAPABILITIES, contextReadCapability, openCapability, accessExplainCapability, contextDelegateCapability, accessRequestCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, contextBounds, resolveOrgScopeId, explainRefusedRead, RESOURCE_HIDDEN_MESSAGE, RESOURCE_UNREADABLE_MESSAGE, type RefusedRead, } from './context.js';
17
18
  export { CONNECTION_CAPABILITIES, connectionProxyCapability, requestConnectionCapability, } from './connections.js';
18
19
  export { DISCOVERY_CAPABILITIES, agentSearchCapability, agentGetCapability, } from './discovery.js';
19
- export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, findArtifactsCapability, type UnsearchedContext, shareArtifactCapability, attachArtifactCapability, reviewArtifactContributionCapability, contributeArtifactCapability, uploadArtifactUrlCapability, completeArtifactFileCapability, downloadArtifactCapability, reextractArtifactCapability, } from './artifacts.js';
20
+ export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, findArtifactsCapability, type UnsearchedContext, attachArtifactCapability, reviewArtifactContributionCapability, contributeArtifactCapability, uploadArtifactUrlCapability, completeArtifactFileCapability, downloadArtifactCapability, reextractArtifactCapability, } from './artifacts.js';
20
21
  export { EVERYDAY_MCP_TOOLS, EVERYDAY_NO_SDK_TWIN, EVERYDAY_SDK_TOOLS, } from './everyday.js';
21
22
  export { annotationForSdkTool } from './toolLane.js';
22
23
  export { presentResourceActions } from './resourceActions.js';
@@ -13,10 +13,11 @@ export { TASK_CAPABILITIES, listTasksCapability, cancelTaskCapability, delegateT
13
13
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
14
14
  export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
15
15
  export { GRANTS_CAPABILITIES, listGrantsCapability } from './grants.js';
16
+ export { BUCKET_CAPABILITIES, bucketCreateCapability, bucketListCapability, bucketReadCapability, bucketInsertCapability, bucketRemoveCapability, bucketMoveCapability, bucketAddMembersCapability, bucketJoinCapability, bucketSendCapability, bucketRemoveMemberCapability, bucketDuplicateCapability, bucketRenameCapability, bucketDeleteCapability, } from './buckets.js';
16
17
  export { CONTEXT_CAPABILITIES, contextReadCapability, openCapability, accessExplainCapability, contextDelegateCapability, accessRequestCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, contextBounds, resolveOrgScopeId, explainRefusedRead, RESOURCE_HIDDEN_MESSAGE, RESOURCE_UNREADABLE_MESSAGE, } from './context.js';
17
18
  export { CONNECTION_CAPABILITIES, connectionProxyCapability, requestConnectionCapability, } from './connections.js';
18
19
  export { DISCOVERY_CAPABILITIES, agentSearchCapability, agentGetCapability, } from './discovery.js';
19
- export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, findArtifactsCapability, shareArtifactCapability, attachArtifactCapability, reviewArtifactContributionCapability, contributeArtifactCapability, uploadArtifactUrlCapability, completeArtifactFileCapability, downloadArtifactCapability, reextractArtifactCapability, } from './artifacts.js';
20
+ export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, findArtifactsCapability, attachArtifactCapability, reviewArtifactContributionCapability, contributeArtifactCapability, uploadArtifactUrlCapability, completeArtifactFileCapability, downloadArtifactCapability, reextractArtifactCapability, } from './artifacts.js';
20
21
  export { EVERYDAY_MCP_TOOLS, EVERYDAY_NO_SDK_TWIN, EVERYDAY_SDK_TOOLS, } from './everyday.js';
21
22
  export { annotationForSdkTool } from './toolLane.js';
22
23
  export { presentResourceActions } from './resourceActions.js';
@@ -66,7 +66,6 @@ export const listLinksCapability = {
66
66
  // is now, as `peer`, resolved relative to whoever asked.
67
67
  const readPlan = active.length ? [
68
68
  nextCall(env, 'message_send', undefined, 'Send an explicit message command to a returned peer.id; choose readers, delivery recipients and the intent (fyi or request) separately. A link grants no history access.'),
69
- nextCall(env, 'artifact_share', undefined, 'Share only the selected artifact under its grant and consent policy.'),
70
69
  ] : [];
71
70
  return {
72
71
  count: links.length,
@@ -165,10 +165,6 @@ const BRIEFS = {
165
165
  ziggs_artifact_find: {
166
166
  does: 'Searches artifacts you can read by name, filename or body. coverage and notSearched say what was and was not searched.',
167
167
  },
168
- ziggs_artifact_share: {
169
- does: 'Shares one artifact you authored with another agent.',
170
- changes: 'grants it at once to a connected agent; otherwise asks your owner to approve (pending_approval)',
171
- },
172
168
  ziggs_message_send: { does: 'Sends a canonical command with independent readers, recipients, an intent (fyi or request) and optional mandate.', changes: 'delivers a message and grants its readers access; a request wakes the agents it reaches' },
173
169
  ziggs_message_replies: { does: 'Reads authorized causal replies by messageId; a reference grants no access and receipt expiry does not erase history.' },
174
170
  ziggs_message_read: { does: 'Reads an exact message by messageId under current access and execution policy. Does not acknowledge its receipt.' },
@@ -291,6 +287,57 @@ const BRIEFS = {
291
287
  does: 'Calls one tool on a connected MCP server through the Ziggs gateway, within your grant. ziggs_connection_proxy refuses MCP servers (Unknown provider: mcp).',
292
288
  changes: 'whatever that tool changes',
293
289
  },
290
+ // Buckets: pointers to items and people; access is computed from the items' grants.
291
+ ziggs_bucket_create: {
292
+ does: 'Creates a bucket, with items if given.',
293
+ changes: 'creates it with what fits',
294
+ },
295
+ ziggs_bucket_list: {
296
+ does: 'Lists the buckets you are in.',
297
+ },
298
+ ziggs_bucket_read: {
299
+ does: 'Reads a bucket and a page of its items; those someone cannot read are set apart.',
300
+ },
301
+ ziggs_bucket_insert: {
302
+ does: 'Puts items in a bucket if everyone in it can read them.',
303
+ changes: 'adds what fits; share may share',
304
+ },
305
+ ziggs_bucket_remove: {
306
+ does: 'Takes items out of a bucket. The items are untouched.',
307
+ changes: 'removes them',
308
+ },
309
+ ziggs_bucket_duplicate: {
310
+ does: 'Copies items into a new bucket for another audience. The original stays.',
311
+ changes: 'creates it with what fits',
312
+ },
313
+ ziggs_bucket_move: {
314
+ does: 'Moves items to another or a new bucket, if its audience can read them.',
315
+ changes: 'moves what fits',
316
+ },
317
+ ziggs_bucket_add_members: {
318
+ does: 'Adds to a bucket audience. Who cannot read an item needs onMisfit share or split.',
319
+ changes: 'adds them',
320
+ },
321
+ ziggs_bucket_join: {
322
+ does: "Brings another bucket's people and items in. Who cannot read an item needs onMisfit share.",
323
+ changes: 'adds them',
324
+ },
325
+ ziggs_bucket_send: {
326
+ does: "Sends a message inside a bucket, to its people.",
327
+ changes: 'sends it',
328
+ },
329
+ ziggs_bucket_rename: {
330
+ does: 'Renames a bucket. Its creator does this.',
331
+ changes: 'the name',
332
+ },
333
+ ziggs_bucket_delete: {
334
+ does: 'Deletes a bucket. The items are untouched. Its creator does this.',
335
+ changes: 'deletes it',
336
+ },
337
+ ziggs_bucket_remove_member: {
338
+ does: 'Takes someone out of a bucket. Its creator does this.',
339
+ changes: 'removes them',
340
+ },
294
341
  ziggs_payment_balance: {
295
342
  does: 'Shows your points balance and what is available after holds. Points settle through work; here they cannot be bought, cashed out or transferred.',
296
343
  },
@@ -30,7 +30,6 @@ const REQUIRED_ARGS = {
30
30
  agreement_subcontract: ['parentAgreementId', 'executorId', 'description'],
31
31
  agreement_respond: ['agreementId'],
32
32
  introduction_redeem: ['token'],
33
- artifact_share: ['artifactId', 'holderId'],
34
33
  context_delegate: ['parentGrantId', 'holderId', 'scopeKind', 'scopeId', 'temporal'],
35
34
  task_create: ['agreementId', 'description'],
36
35
  task_cancel: ['taskId'],
@@ -61,7 +60,6 @@ const HTTP_BINDINGS = {
61
60
  path: '/agreements/:agreementId/approvals/:partyId',
62
61
  },
63
62
  marketplace_view: { method: 'GET', path: '/marketplace/offers' },
64
- artifact_share: { method: 'POST', path: '/context/artifacts/:artifactId/share' },
65
63
  context_delegate: { method: 'POST', path: '/context/grants/:parentGrantId/delegate' },
66
64
  open: { method: 'POST', path: '/context/open' },
67
65
  access_explain: { method: 'POST', path: '/context/access/explain' },
@@ -0,0 +1,219 @@
1
+ import type { MessageCommand, MessageCommandResult } from '@ziggs-ai/contracts';
2
+ /** What a bucket can hold. Each item is a pointer, never a copy. */
3
+ export declare const BUCKET_ITEM_KINDS: readonly ["message", "artifact", "task", "agreement"];
4
+ export type BucketItemKind = (typeof BUCKET_ITEM_KINDS)[number];
5
+ export interface BucketItemRef {
6
+ kind: BucketItemKind;
7
+ id: string;
8
+ }
9
+ /**
10
+ * An item that does not fit where it was asked to go, or no longer fits where
11
+ * it is. Other members are named only when the caller can read the item.
12
+ */
13
+ export type BucketMisfit = {
14
+ item: BucketItemRef;
15
+ reason: 'you-cannot-read';
16
+ } | {
17
+ item: BucketItemRef;
18
+ reason: 'members-cannot-read';
19
+ memberIds: string[];
20
+ };
21
+ /**
22
+ * What to do when someone being added cannot read an item: share it with
23
+ * them, or split: start a narrower bucket with them and only what everyone
24
+ * can read, leaving this one as it is.
25
+ */
26
+ export type BucketMisfitChoice = 'share' | 'split';
27
+ export interface BucketView {
28
+ bucketId: string;
29
+ name: string;
30
+ createdBy: string;
31
+ /** Who it is for, as given: people, agents, workspaces. */
32
+ audience: string[];
33
+ /** Goes up when an item leaves or the audience changes. */
34
+ revision: number;
35
+ /** Items held; whether each still fits is computed when the items are read. */
36
+ itemCount: number;
37
+ createdAt: string | null;
38
+ }
39
+ /** One share made because the caller chose to share. */
40
+ export interface BucketShare {
41
+ item: BucketItemRef;
42
+ holderId: string;
43
+ /** `pending` waits for the item owner's yes; the item stays out until then. */
44
+ outcome: 'shared' | 'pending' | 'refused';
45
+ }
46
+ export interface BucketInsertResult {
47
+ bucket: BucketView;
48
+ inserted: BucketItemRef[];
49
+ alreadyIn: BucketItemRef[];
50
+ misfits: BucketMisfit[];
51
+ shared: BucketShare[];
52
+ }
53
+ export interface BucketMoveResult {
54
+ source: BucketView;
55
+ bucket: BucketView;
56
+ moved: BucketItemRef[];
57
+ notIn: BucketItemRef[];
58
+ misfits: BucketMisfit[];
59
+ }
60
+ export interface BucketAddMembersResult {
61
+ bucket: BucketView;
62
+ added: string[];
63
+ shared: BucketShare[];
64
+ /** The narrower bucket a split started, and what it holds. This bucket is left as it was. */
65
+ split: {
66
+ bucket: BucketView;
67
+ items: BucketItemRef[];
68
+ } | null;
69
+ }
70
+ export interface BucketJoinResult {
71
+ bucket: BucketView;
72
+ added: string[];
73
+ joined: BucketItemRef[];
74
+ shared: BucketShare[];
75
+ }
76
+ /** One thread in a bucket, as the reader sees it. */
77
+ export interface BucketThread {
78
+ rootMessageId: string;
79
+ title: string;
80
+ messageCount: number;
81
+ lastAt: string | null;
82
+ /** The reader wrote in it or it was delivered to them. */
83
+ yours: boolean;
84
+ /** The thread holding the message asked about as `current`. */
85
+ current: boolean;
86
+ /** The reader's own threads, other than the current one, come with their latest messages. */
87
+ recent?: Array<{
88
+ messageId: string;
89
+ from: string;
90
+ text: string;
91
+ at: string;
92
+ }>;
93
+ }
94
+ /** What is in a bucket as the reader can read it now: what a run starts from. */
95
+ export interface BucketContents {
96
+ bucket: {
97
+ bucketId: string;
98
+ name: string;
99
+ audience: string[];
100
+ revision: number;
101
+ };
102
+ threads: BucketThread[];
103
+ artifacts: Array<{
104
+ artifactId: string;
105
+ name: string | null;
106
+ }>;
107
+ tasks: Array<{
108
+ taskId: string;
109
+ title: string;
110
+ state: string;
111
+ }>;
112
+ agreements: Array<{
113
+ agreementId: string;
114
+ title: string;
115
+ status: string;
116
+ }>;
117
+ /** Items held that the reader cannot read here, as they ask now. */
118
+ outOfReach: number;
119
+ }
120
+ /** A message sent inside a bucket: its readers are the bucket's people. */
121
+ export type BucketMessage = Omit<MessageCommand, 'audience'>;
122
+ export interface BucketItemPage {
123
+ /** Items everyone it is for can read right now. */
124
+ items: Array<BucketItemRef & {
125
+ addedBy: string;
126
+ addedAt: string | null;
127
+ }>;
128
+ /** Items held that someone it is for can no longer read. */
129
+ outOfReach: BucketMisfit[];
130
+ nextCursor: string | null;
131
+ }
132
+ /**
133
+ * The bucket operations (`/buckets`). A bucket is pointers to items and to who
134
+ * it is for; it stores no permission. Whether everyone can read each item is
135
+ * computed from the items' own grants whenever it is asked.
136
+ *
137
+ * Every message is sent through a bucket and the bucket points at it; no
138
+ * message names its bucket. `listMine` with an item finds the buckets holding
139
+ * it, the first being where a message was sent.
140
+ *
141
+ * Changing a bucket is not part of a run, so those calls never carry the
142
+ * selected execution. Sending inside a bucket is: it carries the run, and the
143
+ * run's own guards apply to it.
144
+ */
145
+ export declare class BucketsClient {
146
+ private readonly operatorKey;
147
+ private readonly agentId?;
148
+ private readonly baseUrl?;
149
+ constructor(operatorKey: string, agentId?: string | undefined, baseUrl?: string | undefined);
150
+ create(input: {
151
+ name: string;
152
+ audience?: string[];
153
+ items?: BucketItemRef[];
154
+ share?: boolean;
155
+ }): Promise<BucketInsertResult>;
156
+ /** The buckets you are in; given an item, those holding it, in the order it went in. */
157
+ listMine(item?: BucketItemRef): Promise<{
158
+ buckets: BucketView[];
159
+ }>;
160
+ /** Send a message inside a bucket, under the selected run when there is one. */
161
+ sendMessage(bucketId: string, message: BucketMessage): Promise<MessageCommandResult>;
162
+ /**
163
+ * What is in a bucket as you can read it now, under the selected run when
164
+ * there is one: your threads with their latest messages, the rest by title.
165
+ * `current` names the message being answered; its thread is marked.
166
+ */
167
+ contents(bucketId: string, opts?: {
168
+ current?: string;
169
+ }): Promise<BucketContents>;
170
+ /** Bring another bucket's people and items in. The other bucket stays. */
171
+ join(bucketId: string, input: {
172
+ from: string;
173
+ onMisfit?: 'share';
174
+ }): Promise<BucketJoinResult>;
175
+ get(bucketId: string): Promise<BucketView>;
176
+ items(bucketId: string, opts?: {
177
+ before?: string;
178
+ limit?: number;
179
+ }): Promise<BucketItemPage>;
180
+ insert(bucketId: string, items: BucketItemRef[], opts?: {
181
+ share?: boolean;
182
+ }): Promise<BucketInsertResult>;
183
+ remove(bucketId: string, items: BucketItemRef[]): Promise<{
184
+ bucket: BucketView;
185
+ removed: BucketItemRef[];
186
+ notIn: BucketItemRef[];
187
+ }>;
188
+ duplicate(bucketId: string, input: {
189
+ name: string;
190
+ audience?: string[];
191
+ items?: BucketItemRef[];
192
+ share?: boolean;
193
+ }): Promise<BucketInsertResult & {
194
+ notIn: BucketItemRef[];
195
+ }>;
196
+ move(bucketId: string, input: {
197
+ to: string;
198
+ items: BucketItemRef[];
199
+ }): Promise<BucketMoveResult>;
200
+ split(bucketId: string, input: {
201
+ items: BucketItemRef[];
202
+ name: string;
203
+ audience?: string[];
204
+ }): Promise<BucketMoveResult>;
205
+ addMembers(bucketId: string, principalIds: string[], opts?: {
206
+ onMisfit?: BucketMisfitChoice;
207
+ }): Promise<BucketAddMembersResult>;
208
+ removeMember(bucketId: string, principalId: string): Promise<{
209
+ bucket: BucketView;
210
+ removed: boolean;
211
+ }>;
212
+ rename(bucketId: string, name: string): Promise<BucketView>;
213
+ delete(bucketId: string): Promise<{
214
+ bucketId: string;
215
+ deleted: boolean;
216
+ }>;
217
+ private path;
218
+ private request;
219
+ }
@@ -0,0 +1,109 @@
1
+ import { getBackendUrl } from '../utils/urlUtils.js';
2
+ import { throwApiError } from '../shared/apiError.js';
3
+ import { buildOperatorHeaders, withoutExecution } from './operatorHeaders.js';
4
+ /** What a bucket can hold. Each item is a pointer, never a copy. */
5
+ export const BUCKET_ITEM_KINDS = ['message', 'artifact', 'task', 'agreement'];
6
+ /**
7
+ * The bucket operations (`/buckets`). A bucket is pointers to items and to who
8
+ * it is for; it stores no permission. Whether everyone can read each item is
9
+ * computed from the items' own grants whenever it is asked.
10
+ *
11
+ * Every message is sent through a bucket and the bucket points at it; no
12
+ * message names its bucket. `listMine` with an item finds the buckets holding
13
+ * it, the first being where a message was sent.
14
+ *
15
+ * Changing a bucket is not part of a run, so those calls never carry the
16
+ * selected execution. Sending inside a bucket is: it carries the run, and the
17
+ * run's own guards apply to it.
18
+ */
19
+ export class BucketsClient {
20
+ operatorKey;
21
+ agentId;
22
+ baseUrl;
23
+ constructor(operatorKey, agentId, baseUrl) {
24
+ this.operatorKey = operatorKey;
25
+ this.agentId = agentId;
26
+ this.baseUrl = baseUrl;
27
+ if (!operatorKey)
28
+ throw new Error('BucketsClient: operatorKey is required');
29
+ }
30
+ create(input) {
31
+ return this.request('POST', '/buckets', input, 'bucket create');
32
+ }
33
+ /** The buckets you are in; given an item, those holding it, in the order it went in. */
34
+ listMine(item) {
35
+ const query = item ? `?${new URLSearchParams({ kind: item.kind, id: item.id })}` : '';
36
+ return this.request('GET', `/buckets${query}`, undefined, 'bucket list');
37
+ }
38
+ /** Send a message inside a bucket, under the selected run when there is one. */
39
+ sendMessage(bucketId, message) {
40
+ return this.request('POST', `${this.path(bucketId)}/send`, message, 'bucket send', { inRun: true });
41
+ }
42
+ /**
43
+ * What is in a bucket as you can read it now, under the selected run when
44
+ * there is one: your threads with their latest messages, the rest by title.
45
+ * `current` names the message being answered; its thread is marked.
46
+ */
47
+ contents(bucketId, opts = {}) {
48
+ const query = opts.current ? `?${new URLSearchParams({ current: opts.current })}` : '';
49
+ return this.request('GET', `${this.path(bucketId)}/contents${query}`, undefined, 'bucket contents', { inRun: true });
50
+ }
51
+ /** Bring another bucket's people and items in. The other bucket stays. */
52
+ join(bucketId, input) {
53
+ return this.request('POST', `${this.path(bucketId)}/join`, input, 'bucket join');
54
+ }
55
+ get(bucketId) {
56
+ return this.request('GET', this.path(bucketId), undefined, 'bucket read');
57
+ }
58
+ items(bucketId, opts = {}) {
59
+ const query = new URLSearchParams();
60
+ if (opts.before)
61
+ query.set('before', opts.before);
62
+ if (opts.limit !== undefined)
63
+ query.set('limit', String(opts.limit));
64
+ const suffix = query.size ? `?${query}` : '';
65
+ return this.request('GET', `${this.path(bucketId)}/items${suffix}`, undefined, 'bucket items');
66
+ }
67
+ insert(bucketId, items, opts = {}) {
68
+ return this.request('POST', `${this.path(bucketId)}/insert`, { items, ...(opts.share ? { share: true } : {}) }, 'bucket insert');
69
+ }
70
+ remove(bucketId, items) {
71
+ return this.request('POST', `${this.path(bucketId)}/remove`, { items }, 'bucket remove');
72
+ }
73
+ duplicate(bucketId, input) {
74
+ return this.request('POST', `${this.path(bucketId)}/duplicate`, input, 'bucket duplicate');
75
+ }
76
+ move(bucketId, input) {
77
+ return this.request('POST', `${this.path(bucketId)}/move`, input, 'bucket move');
78
+ }
79
+ split(bucketId, input) {
80
+ return this.request('POST', `${this.path(bucketId)}/split`, input, 'bucket split');
81
+ }
82
+ addMembers(bucketId, principalIds, opts = {}) {
83
+ return this.request('POST', `${this.path(bucketId)}/members`, { principalIds, ...(opts.onMisfit ? { onMisfit: opts.onMisfit } : {}) }, 'bucket add members');
84
+ }
85
+ removeMember(bucketId, principalId) {
86
+ return this.request('DELETE', `${this.path(bucketId)}/members/${encodeURIComponent(principalId)}`, undefined, 'bucket remove member');
87
+ }
88
+ rename(bucketId, name) {
89
+ return this.request('PATCH', this.path(bucketId), { name }, 'bucket rename');
90
+ }
91
+ delete(bucketId) {
92
+ return this.request('DELETE', this.path(bucketId), undefined, 'bucket delete');
93
+ }
94
+ path(bucketId) {
95
+ return `/buckets/${encodeURIComponent(bucketId)}`;
96
+ }
97
+ async request(method, path, body, label, opts = {}) {
98
+ const headers = buildOperatorHeaders(this.operatorKey, this.agentId, body === undefined ? undefined : { 'content-type': 'application/json' });
99
+ const res = await fetch(`${this.baseUrl ?? getBackendUrl()}${path}`, {
100
+ method,
101
+ headers: opts.inRun ? headers : withoutExecution(headers),
102
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
103
+ });
104
+ const text = await res.text().catch(() => '');
105
+ if (!res.ok)
106
+ throwApiError(res, text, `${label} failed: ${res.status} ${res.statusText}`);
107
+ return (text ? JSON.parse(text) : {});
108
+ }
109
+ }
@@ -124,22 +124,6 @@ export declare class ContextGrantsClient {
124
124
  * labels-only, grant-fenced server-side.
125
125
  */
126
126
  getReach(grantId: string): Promise<GrantReachResult>;
127
- /**
128
- * share an artifact THIS agent authored with another agent.
129
- *
130
- * Not a delegation: an agent holds no grant over its own output, so there is
131
- * no parent to attenuate. Authorship is the authority, held by the agent's
132
- * owner — which is why the receiver decides the outcome. A receiver already
133
- * inside the sharer's trust boundary (same owner, same org, or an active link)
134
- * is granted immediately; anyone else opens a request the owner approves,
135
- * exactly like `delegateGrant`'s new-party path.
136
- */
137
- shareArtifact(artifactId: string, input: {
138
- holderId: string;
139
- expiresAt?: string | null;
140
- }, opts?: {
141
- idempotencyKey?: string;
142
- }): Promise<DelegateContextGrantResult>;
143
127
  revokeGrant(grantId: string): Promise<{
144
128
  status: string;
145
129
  revokedCount?: number;
@@ -136,33 +136,6 @@ export class ContextGrantsClient {
136
136
  truncatedAgreements: parsed.truncatedAgreements ?? 0,
137
137
  };
138
138
  }
139
- /**
140
- * share an artifact THIS agent authored with another agent.
141
- *
142
- * Not a delegation: an agent holds no grant over its own output, so there is
143
- * no parent to attenuate. Authorship is the authority, held by the agent's
144
- * owner — which is why the receiver decides the outcome. A receiver already
145
- * inside the sharer's trust boundary (same owner, same org, or an active link)
146
- * is granted immediately; anyone else opens a request the owner approves,
147
- * exactly like `delegateGrant`'s new-party path.
148
- */
149
- async shareArtifact(artifactId, input, opts) {
150
- const res = await fetch(`${this.baseUrl}/context/artifacts/${encodeURIComponent(artifactId)}/share`, {
151
- method: 'POST',
152
- headers: buildOperatorHeaders(this.operatorKey, this.agentId, {
153
- 'content-type': 'application/json',
154
- ...(opts?.idempotencyKey
155
- ? { 'Idempotency-Key': opts.idempotencyKey }
156
- : {}),
157
- }),
158
- body: JSON.stringify(input),
159
- });
160
- const body = await res.text().catch(() => '');
161
- if (!res.ok) {
162
- throwApiError(res, body, `shareArtifact failed: ${res.status} ${res.statusText}`);
163
- }
164
- return parseGrantOrPending(body, 'POST /context/artifacts/:id/share');
165
- }
166
139
  async revokeGrant(grantId) {
167
140
  const res = await fetch(`${this.baseUrl}/context/grants/${encodeURIComponent(grantId)}`, {
168
141
  method: 'DELETE',
@@ -28,6 +28,8 @@ export type { MyOrg, OrgResolution } from './OrgsClient.js';
28
28
  export { AgentSearchClient } from './AgentSearchClient.js';
29
29
  export { TelemetryClient } from './TelemetryClient.js';
30
30
  export { InboxClient } from './InboxClient.js';
31
+ export { BucketsClient, BUCKET_ITEM_KINDS } from './BucketsClient.js';
32
+ export type { BucketItemKind, BucketItemRef, BucketItemPage, BucketAddMembersResult, BucketContents, BucketThread, BucketJoinResult, BucketMessage, BucketInsertResult, BucketMisfit, BucketMisfitChoice, BucketMoveResult, BucketShare, BucketView, } from './BucketsClient.js';
31
33
  export { SessionsClient } from './SessionsClient.js';
32
34
  export type { AssistantSessionRow } from './SessionsClient.js';
33
35
  export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from '../inboxAck.js';
@@ -19,6 +19,7 @@ export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentA
19
19
  export { AgentSearchClient } from './AgentSearchClient.js';
20
20
  export { TelemetryClient } from './TelemetryClient.js';
21
21
  export { InboxClient } from './InboxClient.js';
22
+ export { BucketsClient, BUCKET_ITEM_KINDS } from './BucketsClient.js';
22
23
  export { SessionsClient } from './SessionsClient.js';
23
24
  export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from '../inboxAck.js';
24
25
  // The lane header, built in one place — see operatorHeaders.ts. Exported
@@ -12,7 +12,8 @@ export interface OperationError {
12
12
  /**
13
13
  * What the caller may pick from, when the refusal is a choice: the
14
14
  * agreements of WORK_SELECTION_REQUIRED, the workspaces of
15
- * WORKSPACE_SELECTION_REQUIRED. Carried through verbatim so an assistant can
15
+ * WORKSPACE_SELECTION_REQUIRED, the onMisfit values (share, split) of
16
+ * BUCKET_MEMBERS_CANNOT_READ. Carried through verbatim so an assistant can
16
17
  * ask its human instead of re-reading the message.
17
18
  */
18
19
  choices?: unknown[];
@@ -22,6 +23,15 @@ export interface OperationError {
22
23
  * the same shape an open's action recovery has.
23
24
  */
24
25
  recovery?: Record<string, unknown>;
26
+ /**
27
+ * What the refusal is about, when the server said: the misfits of
28
+ * BUCKET_MEMBERS_CANNOT_READ (with any shares made before it stopped), the
29
+ * bucket and missing readers of BUCKET_READERS_MISSING, the live outside
30
+ * engagements that stop a workspace blocking outside agents. Carried
31
+ * through verbatim so an assistant can list
32
+ * them instead of re-reading the message.
33
+ */
34
+ impact?: Record<string, unknown>;
25
35
  /** Scoped restart position returned when too many observation streams exist. */
26
36
  recoveryCursor?: string;
27
37
  /** hidden | unreadable, the same outcome open and access_explain give. */
@@ -64,34 +64,31 @@ function thrownFields(err) {
64
64
  const message = typeof o.message === 'string' && o.message ? o.message : String(err);
65
65
  return { message, status, code, choices: choicesIn(o.body) };
66
66
  }
67
- /** The `recovery` call of a refusal body, when the server sent one. */
68
- function recoveryIn(body) {
67
+ /** A refusal body's fields, when the server sent a JSON object. */
68
+ function refusalBody(body) {
69
69
  if (typeof body !== 'string' || !body)
70
70
  return null;
71
71
  try {
72
72
  const parsed = JSON.parse(body);
73
- const recovery = parsed?.recovery;
74
- if (!recovery || typeof recovery !== 'object' || Array.isArray(recovery))
75
- return null;
76
- return recovery;
73
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
74
+ ? parsed
75
+ : null;
77
76
  }
78
77
  catch {
79
78
  return null;
80
79
  }
81
80
  }
81
+ /** One object field of a refusal body (`recovery`, `impact`), when the server sent it. */
82
+ function objectIn(body, field) {
83
+ const value = refusalBody(body)?.[field];
84
+ return value && typeof value === 'object' && !Array.isArray(value)
85
+ ? value
86
+ : null;
87
+ }
82
88
  /** The `choices` list of a refusal body, when the server sent one. */
83
89
  function choicesIn(body) {
84
- if (typeof body !== 'string' || !body)
85
- return null;
86
- try {
87
- const parsed = JSON.parse(body);
88
- return Array.isArray(parsed?.choices)
89
- ? parsed.choices
90
- : null;
91
- }
92
- catch {
93
- return null;
94
- }
90
+ const choices = refusalBody(body)?.choices;
91
+ return Array.isArray(choices) ? choices : null;
95
92
  }
96
93
  /**
97
94
  * Classify a thrown client error (or a plain string this layer raised) into a
@@ -101,9 +98,9 @@ export function describeOperationError(input, surface = 'sdk') {
101
98
  input = unwrapApiError(input);
102
99
  const shape = classify(input);
103
100
  const { choices } = thrownFields(input);
104
- const recovery = typeof input === 'object' && input !== null
105
- ? recoveryIn(input.body)
106
- : null;
101
+ const body = typeof input === 'object' && input !== null ? input.body : undefined;
102
+ const recovery = objectIn(body, 'recovery');
103
+ const impact = objectIn(body, 'impact');
107
104
  let recoveryCursor;
108
105
  if (shape.code === 'INBOX_OBSERVATION_LIMIT' && input && typeof input === 'object') {
109
106
  try {
@@ -117,6 +114,7 @@ export function describeOperationError(input, surface = 'sdk') {
117
114
  ...shape,
118
115
  ...(recoveryCursor ? { recoveryCursor } : {}),
119
116
  ...(choices ? { choices } : {}),
117
+ ...(impact ? { impact } : {}),
120
118
  ...(recovery ? { recovery: typeof recovery.tool === 'string'
121
119
  ? { ...presentResourceCall(recovery, surface) }
122
120
  : recovery } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.40.4",
3
+ "version": "0.40.5",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "homepage": "https://ziggsai.com/docs",
6
6
  "bugs": {
@@ -31,7 +31,7 @@
31
31
  "test:watch": "node --import tsx/esm --test --watch test/*.test.ts"
32
32
  },
33
33
  "dependencies": {
34
- "@ziggs-ai/contracts": "0.46.0",
34
+ "@ziggs-ai/contracts": "0.48.0",
35
35
  "socket.io-client": "^4.7.0"
36
36
  },
37
37
  "keywords": [