@ziggs-ai/api-client 0.40.4 → 0.40.6

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,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.40.6
6
+
7
+ ## 0.40.5
8
+
9
+ - 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.
10
+ - 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`.
11
+ - 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`).
12
+
5
13
  ## 0.40.4
6
14
 
7
15
  - `InboxEnvelope.bellAt`: the latest delivery a read answered. `getInbox({ seen })` passes it back so a held read answers only mail delivered after it.
@@ -23,6 +23,12 @@ import type { CapabilityDefinition, CapabilityParam } from './types.js';
23
23
  * Hosted-agent tools spread this instead of keeping a shorter copy.
24
24
  */
25
25
  export declare const AGREEMENT_TERMS_PARAMS: Record<string, CapabilityParam>;
26
+ /** The bucket a hire goes into. Exported for the claim verb. */
27
+ export declare const BUCKET_PARAM: CapabilityParam;
28
+ /** The named bucket off a validated arg bag, as the wire field. */
29
+ export declare function bucketFrom(args: Record<string, unknown>): {
30
+ bucketId?: string;
31
+ };
26
32
  export declare const agreementBuyCapability: CapabilityDefinition;
27
33
  export declare const agreementBidCapability: CapabilityDefinition;
28
34
  export declare const agreementBrokerCapability: CapabilityDefinition;
@@ -92,6 +92,16 @@ const MANDATE_PARAM = {
92
92
  type: 'string',
93
93
  description: 'Parent job: an active agreement you participate in. Ending it ends this work. It grants neither hiring nor representation authority.',
94
94
  };
95
+ /** The bucket a hire goes into. Exported for the claim verb. */
96
+ export const BUCKET_PARAM = {
97
+ type: 'string',
98
+ description: "Bucket it goes into. Default: your run's bucket, else a new one.",
99
+ };
100
+ /** The named bucket off a validated arg bag, as the wire field. */
101
+ export function bucketFrom(args) {
102
+ const id = args['bucketId'];
103
+ return typeof id === 'string' && id.trim() ? { bucketId: id.trim() } : {};
104
+ }
95
105
  /** Pull the declared mandate off a validated arg bag, as the wire field. */
96
106
  function mandateFrom(args) {
97
107
  const declared = args['parentAgreementId'] ?? args['mandateAgreementId'];
@@ -162,6 +172,7 @@ export const agreementBuyCapability = {
162
172
  addressId: CHAT_PARAM,
163
173
  ...AGREEMENT_TERMS_PARAMS,
164
174
  mandateAgreementId: MANDATE_PARAM,
175
+ bucketId: BUCKET_PARAM,
165
176
  parentAgreementId: { type: 'string', description: 'The job this work is part of; ending it ends this work. Not a link, and no spending consent.' },
166
177
  },
167
178
  needsAgentId: true,
@@ -170,6 +181,7 @@ export const agreementBuyCapability = {
170
181
  const agreement = await proposeDirectTo({
171
182
  ...termsFrom(args),
172
183
  ...mandateFrom(args),
184
+ ...bucketFrom(args),
173
185
  proposedTo: counterparty,
174
186
  addressId: args['addressId'],
175
187
  // They provide. The payer is derived server-side as the other side.
@@ -193,6 +205,7 @@ export const agreementBidCapability = {
193
205
  addressId: CHAT_PARAM,
194
206
  ...AGREEMENT_TERMS_PARAMS,
195
207
  mandateAgreementId: MANDATE_PARAM,
208
+ bucketId: BUCKET_PARAM,
196
209
  parentAgreementId: { type: 'string', description: 'The job this work is part of; ending it ends this work. Not a link, and no spending consent.' },
197
210
  },
198
211
  needsAgentId: true,
@@ -201,6 +214,7 @@ export const agreementBidCapability = {
201
214
  const agreement = await proposeDirectTo({
202
215
  ...termsFrom(args),
203
216
  ...mandateFrom(args),
217
+ ...bucketFrom(args),
204
218
  proposedTo: args['counterparty'],
205
219
  addressId: args['addressId'],
206
220
  // You provide, so the counterparty pays.
@@ -262,6 +276,7 @@ export const agreementRequestCapability = {
262
276
  ...AGREEMENT_TERMS_PARAMS,
263
277
  match: MATCH_PARAM,
264
278
  mandateAgreementId: MANDATE_PARAM,
279
+ bucketId: BUCKET_PARAM,
265
280
  parentAgreementId: { type: 'string', description: 'The job this work is part of; ending it ends this work. Not a link, and no spending consent.' },
266
281
  },
267
282
  needsAgentId: true,
@@ -269,6 +284,7 @@ export const agreementRequestCapability = {
269
284
  const agreement = await proposeBroadcast({
270
285
  ...termsFrom(args),
271
286
  ...mandateFrom(args),
287
+ ...bucketFrom(args),
272
288
  addressId: '',
273
289
  audience: args['audience'],
274
290
  engagementKind: engagementKindFrom(args),
@@ -392,6 +408,7 @@ export const agreementSubcontractCapability = {
392
408
  type: 'string',
393
409
  description: 'Who pays for the subcontracted work. Defaults to the delegating side.',
394
410
  },
411
+ bucketId: BUCKET_PARAM,
395
412
  },
396
413
  needsAgentId: true,
397
414
  handler: async (args, env) => {
@@ -417,6 +434,7 @@ export const agreementSubcontractCapability = {
417
434
  ...(args['payerId'] === undefined
418
435
  ? {}
419
436
  : { payerId: args['payerId'] }),
437
+ ...bucketFrom(args),
420
438
  }, fullCreds(env));
421
439
  return { agreement };
422
440
  },
@@ -1,6 +1,7 @@
1
1
  import { agreementAppUrl } from '../utils/appUrls.js';
2
2
  import { claimOpenAgreement } from '../http/agreementFlows.js';
3
3
  import { linkIsReachOnly, webAppOrigin } from './links.js';
4
+ import { BUCKET_PARAM, bucketFrom } from './agreementVerbs.js';
4
5
  import { workContextFromEnv } from './nextCall.js';
5
6
  import { fullCreds } from './types.js';
6
7
  /** Plain outcome once the hire is active. `kind` stays on the result, not in this sentence. */
@@ -86,11 +87,12 @@ export const agreementClaimCapability = {
86
87
  enum: ['me', 'workspace'],
87
88
  description: "Listing hire: me (default) keeps work private; workspace shares with that workspace. Ask your person if unclear.",
88
89
  },
90
+ bucketId: BUCKET_PARAM,
89
91
  },
90
92
  needsAgentId: true,
91
93
  handler: async (args, env) => {
92
94
  const declaredMandate = args['mandateAgreementId'];
93
- const { agreement, kind } = await claimOpenAgreement(args['agreementId'], fullCreds(env), { ...(typeof declaredMandate === 'string' && declaredMandate ? { mandateAgreementId: declaredMandate } : {}), ...(typeof args['parentAgreementId'] === 'string' ? { parentAgreementId: args['parentAgreementId'] } : {}), ...(claimConnections(args['connections']) ?? {}), ...(args['hireFor'] === 'workspace' ? { hireFor: 'workspace' } : {}) });
95
+ const { agreement, kind } = await claimOpenAgreement(args['agreementId'], fullCreds(env), { ...(typeof declaredMandate === 'string' && declaredMandate ? { mandateAgreementId: declaredMandate } : {}), ...(typeof args['parentAgreementId'] === 'string' ? { parentAgreementId: args['parentAgreementId'] } : {}), ...(claimConnections(args['connections']) ?? {}), ...(args['hireFor'] === 'workspace' ? { hireFor: 'workspace' } : {}), ...bucketFrom(args) });
94
96
  return presentClaimResult(agreement, kind, env);
95
97
  },
96
98
  };
@@ -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
+ ];