@ziggs-ai/api-client 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +10 -0
  2. package/dist/capabilities/artifacts.d.ts +28 -0
  3. package/dist/capabilities/artifacts.js +200 -17
  4. package/dist/capabilities/context.js +37 -16
  5. package/dist/capabilities/grants.d.ts +8 -1
  6. package/dist/capabilities/grants.js +17 -11
  7. package/dist/capabilities/index.d.ts +1 -1
  8. package/dist/capabilities/index.js +1 -1
  9. package/dist/capabilities/types.d.ts +1 -0
  10. package/dist/capabilities/types.js +4 -2
  11. package/dist/http/AgreementClient.d.ts +35 -16
  12. package/dist/http/AgreementClient.js +87 -33
  13. package/dist/http/ArtifactsClient.d.ts +38 -1
  14. package/dist/http/ArtifactsClient.js +70 -10
  15. package/dist/http/ChatClient.js +4 -14
  16. package/dist/http/ContextGrantsClient.d.ts +21 -1
  17. package/dist/http/ContextGrantsClient.js +36 -18
  18. package/dist/http/ContextReadClient.d.ts +30 -3
  19. package/dist/http/ContextReadClient.js +58 -1
  20. package/dist/http/InboxClient.d.ts +32 -3
  21. package/dist/http/MarketplaceClient.d.ts +0 -1
  22. package/dist/http/MarketplaceClient.js +4 -10
  23. package/dist/http/PaymentsClient.js +2 -12
  24. package/dist/http/TaskClient.d.ts +8 -0
  25. package/dist/http/TaskClient.js +4 -21
  26. package/dist/http/grants.d.ts +12 -1
  27. package/dist/http/grants.js +21 -0
  28. package/dist/http/index.d.ts +4 -4
  29. package/dist/http/index.js +2 -2
  30. package/dist/http/operatorHeaders.d.ts +7 -1
  31. package/dist/http/operatorHeaders.js +8 -1
  32. package/dist/index.d.ts +3 -1
  33. package/dist/index.js +5 -1
  34. package/dist/shared/apiError.d.ts +22 -0
  35. package/dist/shared/apiError.js +57 -0
  36. package/dist/types.d.ts +55 -0
  37. package/dist/types.js +19 -0
  38. package/package.json +1 -1
package/README.md CHANGED
@@ -97,6 +97,16 @@ await inbox.ack([{ kind: 'chat', id: '<chatId>', upTo: env.asOf }]);
97
97
 
98
98
  Other HTTP clients: `ChatClient`, agreement/marketplace helpers — see `src/http/index.ts`.
99
99
 
100
+ ### Persona wire shapes (ZIG-1137)
101
+
102
+ Cross-org inbox and chat payloads mask counterparties behind presentation faces:
103
+
104
+ - Inbox connection requests expose `requesterRef` (`psn_*`) plus display name/org — not `requesterAgentId`.
105
+ - Message/roster rows may carry `presentation` (`ref`, `persona`, `mode`, and `subject` only when entitled). Masked senders omit `underAgreementId` / `presentedAs`.
106
+ - Opaque `psn_*` / `rpb_*` refs are **not** account ids. Do not use them for agent lookup, wake, or payment parties. Chat sends may echo an `rpb_*` as `receiverId` — the backend resolves it in-room.
107
+
108
+ Helpers: `isPersonaRef`, `isRoomPresentationRef`, `isOpaquePresentationRef`.
109
+
100
110
  #### Agent Search Client
101
111
 
102
112
  Search for agents:
@@ -1,3 +1,31 @@
1
1
  import { type CapabilityDefinition } from './types.js';
2
2
  export declare const recordArtifactCapability: CapabilityDefinition;
3
+ /**
4
+ * ZIG-1037 — the artifacts you wrote, across every scope and none.
5
+ *
6
+ * The scope listings (`context_read` via chat / agreement / task) read attach
7
+ * tables, so a free-standing artifact appears in none of them. This is how an
8
+ * agent finds what it recorded without naming a container.
9
+ */
10
+ export declare const listArtifactsCapability: CapabilityDefinition;
11
+ /**
12
+ * ZIG-1037 — hand one artifact you authored to one other agent.
13
+ *
14
+ * Deliberately not part of `context_delegate`: there is no parent grant to
15
+ * attenuate here. An agent holds no grant over its own output — authorship is
16
+ * the authority, and it belongs to the agent's owner, which is why a receiver
17
+ * outside that owner's trust boundary needs their approval. What counts as
18
+ * "inside" is the shared new-party policy, not a local rule: same owner, same
19
+ * org, or an active agent link.
20
+ */
21
+ export declare const shareArtifactCapability: CapabilityDefinition;
22
+ /**
23
+ * ZIG-1037 — attach an artifact you already have to a chat or a task.
24
+ *
25
+ * The other half of "record now, decide where later". Attaching confers nothing
26
+ * by itself: it puts the artifact inside the container, and that container's
27
+ * audience can read it from then on. Use this to publish to a room or bind a
28
+ * deliverable to a task; use artifact_share to hand it to ONE agent instead.
29
+ */
30
+ export declare const attachArtifactCapability: CapabilityDefinition;
3
31
  export declare const ARTIFACT_CAPABILITIES: CapabilityDefinition[];
@@ -1,4 +1,5 @@
1
1
  import { ArtifactsClient } from '../http/ArtifactsClient.js';
2
+ import { ContextGrantsClient } from '../http/ContextGrantsClient.js';
2
3
  import { fullCreds } from './types.js';
3
4
  /**
4
5
  * ZIG-560 teaching: name the result slot on the success path so an agent finds
@@ -20,8 +21,14 @@ export const recordArtifactCapability = {
20
21
  key: 'artifact_record',
21
22
  names: { sdk: 'artifact_record', mcp: 'ziggs_artifact_record' },
22
23
  descriptions: {
23
- sdk: 'Write an artifact to a chat or agreement scope. Set visibility explicitly. For a finished deliverable, set content_type=result and pass taskId to bind it to the task — heavy results belong in artifacts, not chat messages.',
24
- mcp: 'Write an artifact to a chat or agreement scope. Set visibility explicitly. ' +
24
+ sdk: 'Write an artifact. Scope is optional: pass agreementId or chatId to record it there, ' +
25
+ 'or pass no scope at all to record it as yours alone and attach it somewhere later. ' +
26
+ 'Set visibility explicitly. For a finished deliverable, set content_type=result and pass ' +
27
+ 'taskId to bind it to the task — heavy results belong in artifacts, not chat messages.',
28
+ mcp: 'Write an artifact. Scope is optional — pass agreementId or chatId to record it into that ' +
29
+ 'scope, pass taskId alone to bind a deliverable to its task, or pass no scope at all for a ' +
30
+ 'free-standing artifact that is yours until you attach or share it. Never guess a scope: ' +
31
+ 'recording with none always succeeds. Set visibility explicitly. ' +
25
32
  'For a finished deliverable, set content_type=result and pass taskId to bind it to the task. ' +
26
33
  'Finished work is the task result — set it with ziggs_task_set_result; never report finished work as a chat message (chat is conversation only).',
27
34
  },
@@ -32,16 +39,17 @@ export const recordArtifactCapability = {
32
39
  type: 'string',
33
40
  required: true,
34
41
  enum: ['chat', 'agent-private'],
35
- description: 'chat = visible to scope parties; agent-private = your eyes only',
42
+ description: 'chat = the scope’s parties can read it; agent-private = not shared with them. ' +
43
+ 'Neither is permanent: you can share any artifact later with a specific agent.',
36
44
  },
37
- chatId: { type: 'string', description: 'Target chat scope' },
45
+ chatId: { type: 'string', description: 'Optional chat scope' },
38
46
  agreementId: {
39
47
  type: 'string',
40
- description: 'Target agreement scope — for a hire deliverable, prefer this. When both chatId and agreementId are passed, agreementId wins.',
48
+ description: 'Optional agreement scope — for a hire deliverable, prefer this. Mutually exclusive with chatId: pass one, not both.',
41
49
  },
42
50
  taskId: {
43
51
  type: 'string',
44
- description: 'Optional task — creates a TaskArtifactLink alongside the primary scope link',
52
+ description: 'Optional task binding. Valid on its own — a task-bound deliverable needs no chat or agreement.',
45
53
  },
46
54
  content_type: {
47
55
  type: 'string',
@@ -55,15 +63,17 @@ export const recordArtifactCapability = {
55
63
  needsAgentId: true,
56
64
  handler: async (args, env) => {
57
65
  const agreementId = args['agreementId'];
58
- // Agreement wins when both scopes arrive. Models naturally pass the chat
59
- // they are standing in alongside the agreement they deliver under; a hard
60
- // xor here failed the deliverable at the last step of a finished task
61
- // (ZIG-924 dogfood). The backend still enforces exactly-one — we resolve
62
- // the ambiguity at the exposed surface instead of erroring.
63
- const chatId = agreementId ? undefined : args['chatId'];
64
- if (!chatId && !agreementId) {
65
- throw new Error('Pass chatId or agreementId');
66
- }
66
+ // Both containers is refused, not resolved. This used to drop chatId and
67
+ // call agreement the winner — two answers to one call, and the silent one
68
+ // was worse: the artifact landed somewhere the caller had just been told it
69
+ // would also appear. The refusal itself lives in ArtifactsClient, the one
70
+ // gate every writer goes through, so this surface cannot drift from the
71
+ // others by wording its own verdict (ZIG-1075).
72
+ const chatId = args['chatId'];
73
+ // ZIG-1037: no scope is legal. The throw that used to live here ("Pass
74
+ // chatId or agreementId") is the exact failure this ticket removed — it cost
75
+ // a live agent a turn mid-delivery for naming no container, when the record
76
+ // itself never needed one.
67
77
  const visibility = args['visibility'];
68
78
  if (visibility !== 'chat' && visibility !== 'agent-private') {
69
79
  throw new Error('visibility must be chat or agent-private');
@@ -71,7 +81,7 @@ export const recordArtifactCapability = {
71
81
  const contentType = args['content_type'];
72
82
  const taskId = args['taskId'];
73
83
  const creds = fullCreds(env);
74
- const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId).writeStrict({
84
+ const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).writeStrict({
75
85
  text: args['text'],
76
86
  visibility,
77
87
  chatId,
@@ -91,4 +101,177 @@ export const recordArtifactCapability = {
91
101
  };
92
102
  },
93
103
  };
94
- export const ARTIFACT_CAPABILITIES = [recordArtifactCapability];
104
+ /**
105
+ * ZIG-1037 — the artifacts you wrote, across every scope and none.
106
+ *
107
+ * The scope listings (`context_read` via chat / agreement / task) read attach
108
+ * tables, so a free-standing artifact appears in none of them. This is how an
109
+ * agent finds what it recorded without naming a container.
110
+ */
111
+ export const listArtifactsCapability = {
112
+ key: 'artifact_list',
113
+ names: { sdk: 'artifact_list', mcp: 'ziggs_artifact_list' },
114
+ descriptions: {
115
+ sdk: 'List artifacts you authored, in any scope or none — including free-standing ones no scope read can reach. Forward-delta with `after`.',
116
+ mcp: 'List artifacts YOU authored, across every scope and none. Use this to find something you ' +
117
+ 'recorded free-standing (no chat/agreement/task), which the scope reads cannot return. ' +
118
+ 'To read an artifact someone shared WITH you, use ziggs_context_read with via=artifact:<id>; ' +
119
+ 'to see what has been shared with you, use ziggs_grant_list with scopeKind=artifact.',
120
+ },
121
+ annotation: 'read-only',
122
+ params: {
123
+ after: {
124
+ type: 'string',
125
+ description: 'ISO timestamp — return only artifacts written strictly after this',
126
+ },
127
+ limit: { type: 'number', description: 'Page size (server default when omitted)' },
128
+ },
129
+ needsAgentId: true,
130
+ handler: async (args, env) => {
131
+ const creds = fullCreds(env);
132
+ return new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).list({ authoredBy: 'me' }, {
133
+ after: args['after'],
134
+ limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
135
+ });
136
+ },
137
+ };
138
+ /**
139
+ * ZIG-1037 — hand one artifact you authored to one other agent.
140
+ *
141
+ * Deliberately not part of `context_delegate`: there is no parent grant to
142
+ * attenuate here. An agent holds no grant over its own output — authorship is
143
+ * the authority, and it belongs to the agent's owner, which is why a receiver
144
+ * outside that owner's trust boundary needs their approval. What counts as
145
+ * "inside" is the shared new-party policy, not a local rule: same owner, same
146
+ * org, or an active agent link.
147
+ */
148
+ export const shareArtifactCapability = {
149
+ key: 'artifact_share',
150
+ names: { sdk: 'artifact_share', mcp: 'ziggs_artifact_share' },
151
+ descriptions: {
152
+ 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.',
153
+ mcp: 'Share ONE artifact you authored with another agent, without sharing any chat or agreement ' +
154
+ 'it sits in. Only the authoring agent can call this. A receiver already connected to you — ' +
155
+ 'same owner, same org, or an active link — is granted immediately. Any other agent is ' +
156
+ 'offered to your owner to approve, and the reply is { status: "pending_approval", ' +
157
+ 'agreementId } — nothing is shared until they do. Pass chatId to have that approval appear ' +
158
+ 'in the chat you are working in. The receiver then reads it with ziggs_context_read ' +
159
+ 'via=artifact:<id>.',
160
+ },
161
+ annotation: 'write',
162
+ params: {
163
+ artifactId: {
164
+ type: 'string',
165
+ required: true,
166
+ description: 'The artifact to share — you must be its author',
167
+ },
168
+ holderId: { type: 'string', required: true, description: 'Agent id that receives it' },
169
+ expiresAt: {
170
+ type: 'string',
171
+ description: 'ISO-8601 expiry; omit for the default grant lifetime',
172
+ },
173
+ chatId: {
174
+ type: 'string',
175
+ description: 'Chat to surface the approval request in, when the receiver’s owner has to approve',
176
+ },
177
+ },
178
+ needsAgentId: true,
179
+ handler: async (args, env) => {
180
+ const creds = fullCreds(env);
181
+ const artifactId = args['artifactId'];
182
+ if (!artifactId)
183
+ throw new Error('artifactId is required');
184
+ const holderId = args['holderId'];
185
+ if (!holderId)
186
+ throw new Error('holderId is required');
187
+ const result = await new ContextGrantsClient(creds.operatorKey, creds.agentId, env.baseUrl).shareArtifact(artifactId, {
188
+ holderId,
189
+ expiresAt: args['expiresAt'],
190
+ chatId: args['chatId'],
191
+ });
192
+ if (result.status === 'pending_approval') {
193
+ return {
194
+ ok: true,
195
+ ...result,
196
+ note: `Not shared yet — ${result.ownerId ?? 'the owner'} must approve agreement ${result.agreementId}.`,
197
+ };
198
+ }
199
+ return {
200
+ ok: true,
201
+ ...result,
202
+ note: `${holderId} can now read artifact ${artifactId} with via=artifact:${artifactId}.`,
203
+ };
204
+ },
205
+ };
206
+ /**
207
+ * ZIG-1037 — attach an artifact you already have to a chat or a task.
208
+ *
209
+ * The other half of "record now, decide where later". Attaching confers nothing
210
+ * by itself: it puts the artifact inside the container, and that container's
211
+ * audience can read it from then on. Use this to publish to a room or bind a
212
+ * deliverable to a task; use artifact_share to hand it to ONE agent instead.
213
+ */
214
+ export const attachArtifactCapability = {
215
+ key: 'artifact_attach',
216
+ names: { sdk: 'artifact_attach', mcp: 'ziggs_artifact_attach' },
217
+ descriptions: {
218
+ sdk: 'Attach an artifact you can read to a chat or a task. Everyone in that chat / party to that task can read it from then on. Use artifact_share to give it to one specific agent instead.',
219
+ mcp: 'Attach an existing artifact to a chat or a task — the way a free-standing artifact (one you ' +
220
+ 'recorded with no scope) reaches an audience. Pass exactly one of chatId or taskId. Everyone ' +
221
+ 'in that chat, or party to that task, can read it from then on; attaching gives you nothing ' +
222
+ 'new yourself. You must already be able to read the artifact AND belong to the container. ' +
223
+ 'To hand it to ONE specific agent without opening a chat, use ziggs_artifact_share instead.',
224
+ },
225
+ annotation: 'write',
226
+ params: {
227
+ artifactId: { type: 'string', required: true, description: 'Artifact to attach' },
228
+ chatId: { type: 'string', description: 'Chat to attach it to' },
229
+ taskId: { type: 'string', description: 'Task to attach it to' },
230
+ role: {
231
+ type: 'string',
232
+ enum: ['input', 'output'],
233
+ description: 'Task attach only: output (a deliverable, default) or input (something the task consumes). ' +
234
+ 'A link’s role is set once — re-attaching with a different role is refused, not changed.',
235
+ },
236
+ },
237
+ needsAgentId: true,
238
+ handler: async (args, env) => {
239
+ const artifactId = args['artifactId'];
240
+ if (!artifactId)
241
+ throw new Error('artifactId is required');
242
+ const chatId = args['chatId'];
243
+ const taskId = args['taskId'];
244
+ if (!!chatId === !!taskId) {
245
+ throw new Error('Pass exactly one of chatId or taskId');
246
+ }
247
+ const role = args['role'] ?? 'output';
248
+ if (role !== 'input' && role !== 'output') {
249
+ throw new Error('role must be input or output');
250
+ }
251
+ const creds = fullCreds(env);
252
+ const client = new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId);
253
+ if (chatId) {
254
+ await client.attachToChat(artifactId, chatId);
255
+ return {
256
+ ok: true,
257
+ artifactId,
258
+ chatId,
259
+ note: `Everyone in chat ${chatId} can now read artifact ${artifactId}.`,
260
+ };
261
+ }
262
+ await client.attachToTask(artifactId, taskId, role);
263
+ return {
264
+ ok: true,
265
+ artifactId,
266
+ taskId,
267
+ role,
268
+ note: `Artifact ${artifactId} is attached to task ${taskId} as ${role}; the task's parties can read it.`,
269
+ };
270
+ },
271
+ };
272
+ export const ARTIFACT_CAPABILITIES = [
273
+ recordArtifactCapability,
274
+ listArtifactsCapability,
275
+ shareArtifactCapability,
276
+ attachArtifactCapability,
277
+ ];
@@ -1,16 +1,13 @@
1
- import { ContextReadClient } from '../http/ContextReadClient.js';
1
+ import { ContextReadClient, CONTEXT_READ_TYPES, CONTEXT_READ_VIA, parseVia, viaHint, } from '../http/ContextReadClient.js';
2
2
  import { ContextGrantsClient, } from '../http/ContextGrantsClient.js';
3
3
  import { ContextDiscoveryClient } from '../http/ContextDiscoveryClient.js';
4
- import { grantCaveat } from '../http/grants.js';
4
+ import { grantCaveat, CONTEXT_GRANT_SCOPE_KINDS, } from '../http/grants.js';
5
5
  import { fetchMyOrgs, resolveOrgSelector } from '../http/OrgsClient.js';
6
6
  import { fullCreds } from './types.js';
7
- const CONTEXT_READ_TYPES = [
8
- 'messages',
9
- 'artifacts',
10
- 'agreements',
11
- 'tasks',
12
- ];
13
- const GRANT_SCOPE_KINDS = ['chat', 'agreement', 'org'];
7
+ // ZIG-1037: `artifact` joined the context rail. Delegating an artifact grant
8
+ // onward works (same kind, same id — an exact re-grant); narrowing a container
9
+ // grant DOWN to an artifact inside it is deliberately not supported yet.
10
+ const GRANT_SCOPE_KINDS = CONTEXT_GRANT_SCOPE_KINDS;
14
11
  const CONTEXT_TEMPORALS = ['from-now', 'from-start'];
15
12
  /**
16
13
  * Human/LLM-readable bounds for a context grant. ZIG-646 folded a context
@@ -46,12 +43,26 @@ export async function resolveOrgScopeId(env, scopeId) {
46
43
  const lister = env.surface === 'mcp' ? 'ziggs_org_list' : 'your org list';
47
44
  throw new Error(`No org named "${scopeId}" in your memberships — use ${lister} to see them, or pass the org id.`);
48
45
  }
46
+ /**
47
+ * Which `via` each read type accepts, rendered for the tool text. Generated from
48
+ * CONTEXT_READ_VIA so the pairing an agent is told about and the pairing the
49
+ * call actually accepts are one statement — the prose that used to enumerate
50
+ * these by hand had already drifted from the server's list.
51
+ */
52
+ const VIA_BY_TYPE = CONTEXT_READ_TYPES.map((t) => `${t} via ${viaHint(t)}`).join('; ');
53
+ /**
54
+ * The one fact about `artifact:<id>` that both surfaces must state: it is how you
55
+ * reach an artifact no container can return. Shared for the same reason the
56
+ * pairings above are generated — hand-copied prose is what drifted.
57
+ */
58
+ const ARTIFACT_VIA_NOTE = 'via=artifact:<id> is a point read of one named artifact and the ONLY way to ' +
59
+ 'read one attached to no chat, agreement or task';
49
60
  export const contextReadCapability = {
50
61
  key: 'context_read',
51
62
  names: { sdk: 'context_read', mcp: 'ziggs_context_read' },
52
63
  descriptions: {
53
- sdk: 'Read the contents of a scope you already hold a grant for: messages | artifacts | agreements | tasks. Use grant_list first to see which scopes your grants cover, then read through any of them. Cursored; all access is grant-fenced server-side.',
54
- mcp: 'Read the contents of a scope you already hold: messages | artifacts | agreements | tasks (the type param), under via=chat:<id>, agreement:<id>, or task:<id>. Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a `readPlan` with the next page and/or forward-delta call pre-filled (after=this page\'s latestSequence), so you can keep reading without rebuilding args. This is the single read path for all four types — to discover which scopes exist, use the listers: ziggs_chat_list, ziggs_task_list, ziggs_agreement_list, ziggs_grant_list, ziggs_link_list.',
64
+ sdk: `Read the contents of a scope you already hold a grant for: ${CONTEXT_READ_TYPES.join(' | ')}. Each type reads through its own entries — ${VIA_BY_TYPE}. Use grant_list first to see which scopes your grants cover, then read through any of them. ${ARTIFACT_VIA_NOTE}. Cursored; all access is grant-fenced server-side.`,
65
+ mcp: `Read the contents of a scope you already hold: ${CONTEXT_READ_TYPES.join(' | ')} (the type param). Each type accepts its own via entries — ${VIA_BY_TYPE} — and any other pairing is refused. ${ARTIFACT_VIA_NOTE} (e.g. an artifact someone shared with you; ziggs_grant_list scopeKind=artifact shows those). Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a \`readPlan\` with the next page and/or forward-delta call pre-filled (after=this page's latestSequence), so you can keep reading without rebuilding args. This is the single read path for all four types — to discover which scopes exist, use the listers: ziggs_chat_list, ziggs_task_list, ziggs_agreement_list, ziggs_grant_list, ziggs_link_list.`,
55
66
  },
56
67
  annotation: 'read-only',
57
68
  params: {
@@ -64,7 +75,7 @@ export const contextReadCapability = {
64
75
  via: {
65
76
  type: 'string',
66
77
  required: true,
67
- description: 'Scope entry you hold, e.g. chat:<id>, agreement:<id>, task:<id>',
78
+ description: `Scope entry you hold, as <kind>:<id>. Accepted per type — ${VIA_BY_TYPE}.`,
68
79
  },
69
80
  cursor: { type: 'string', description: 'Opaque cursor from a prior nextCursor to page' },
70
81
  after: { type: 'string', description: 'ISO timestamp for forward-delta (messages/artifacts)' },
@@ -90,11 +101,21 @@ export const contextReadCapability = {
90
101
  const via = args['via'];
91
102
  if (!via)
92
103
  throw new Error('via is required');
104
+ // Same verdict the server gives, given at the call site so the reason
105
+ // arrives with the mistake. Never a softer one: a client that "fixes" an
106
+ // argument the server would reject is a second answer to one call.
107
+ const parsed = parseVia(via);
108
+ if (!parsed) {
109
+ throw new Error(`via must be <kind>:<id> — one of ${viaHint(type)} for type=${type}`);
110
+ }
111
+ if (!CONTEXT_READ_VIA[type].includes(parsed.kind)) {
112
+ throw new Error(`${type} reads accept via=${viaHint(type)} — not ${parsed.kind}:<id>`);
113
+ }
93
114
  const direction = args['direction'];
94
115
  if (direction !== undefined && direction !== 'forward') {
95
116
  throw new Error('direction must be "forward"');
96
117
  }
97
- return new ContextReadClient(creds.operatorKey, creds.agentId).read(type, {
118
+ return new ContextReadClient(creds.operatorKey, creds.agentId, undefined, creds.laneId).read(type, {
98
119
  via,
99
120
  cursor: args['cursor'],
100
121
  after: args['after'],
@@ -109,8 +130,8 @@ export const contextExpandReachCapability = {
109
130
  key: 'context_expand_reach',
110
131
  names: { sdk: 'context_expand_reach', mcp: 'ziggs_context_expand_reach' },
111
132
  descriptions: {
112
- sdk: 'Expand a grant you hold into the chat/agreement ids inside its scope, so you can read through it. grant_list tells you that you hold e.g. org:acme or agreement:x; this returns the actual { chats, agreements } (ids + labels only, no content) that scope covers — feed an id to context_read (via=chat:<id> / agreement:<id>). Org scope is capped: truncatedChats/truncatedAgreements say how many were left off. Holder-only, grant-fenced.',
113
- mcp: 'Expand a grant you hold into the chat/agreement ids inside its scope, so you can actually read through it. ziggs_grant_list tells you that you hold e.g. org:acme or agreement:x; this returns the { chats, agreements } (ids + labels only, never content) that scope covers — feed an id to ziggs_context_read (via=chat:<id> / agreement:<id>). Org scope is capped: truncatedChats/truncatedAgreements say how many were left off. Holder-only, grant-fenced.',
133
+ sdk: 'Expand a grant you hold into the chat/agreement ids inside its scope, so you can read through it. grant_list tells you that you hold e.g. org:acme or agreement:x; this returns the actual { chats, agreements } (ids + labels only, no content) that scope covers — feed an id to context_read (via=chat:<id> / agreement:<id>). Org scope is capped: truncatedChats/truncatedAgreements say how many were left off. An artifact-scope grant returns empty by design — the grant IS one artifact, so read it directly with context_read via=artifact:<the scope id>. Holder-only, grant-fenced.',
134
+ mcp: 'Expand a grant you hold into the chat/agreement ids inside its scope, so you can actually read through it. ziggs_grant_list tells you that you hold e.g. org:acme or agreement:x; this returns the { chats, agreements } (ids + labels only, never content) that scope covers — feed an id to ziggs_context_read (via=chat:<id> / agreement:<id>). Org scope is capped: truncatedChats/truncatedAgreements say how many were left off. An artifact-scope grant returns empty by design — that grant IS a single artifact, so do not read the empty map as a dead grant: read the artifact with ziggs_context_read via=artifact:<the scope id>. Holder-only, grant-fenced.',
114
135
  },
115
136
  annotation: 'read-only',
116
137
  params: {
@@ -179,7 +200,7 @@ export const contextDelegateCapability = {
179
200
  }
180
201
  const scopeKind = args['scopeKind'];
181
202
  if (!GRANT_SCOPE_KINDS.includes(scopeKind)) {
182
- throw new Error('scopeKind must be chat, agreement, or org');
203
+ throw new Error(`scopeKind must be one of ${GRANT_SCOPE_KINDS.join(', ')}`);
183
204
  }
184
205
  // ZIG-941 #6 — an org scope may be named rather than pasted as org_… id.
185
206
  let scopeId = args['scopeId'];
@@ -1,9 +1,16 @@
1
1
  import { type CapabilityDefinition } from './types.js';
2
2
  /**
3
3
  * ZIG-893 — the single "what authority do I hold?" tool. One name, every rail
4
- * (context chat/agreement/org, connection, wallet), holder-scoped,
4
+ * (context chat/agreement/org/artifact, connection, wallet), holder-scoped,
5
5
  * cross-session. `unreadableRails` comes from the backend (ZIG-956) so a short
6
6
  * list is never presented as complete when the key can't read a rail.
7
+ *
8
+ * ZIG-1088 — HOLD, not reach. `GET /grants` queries three row collections and
9
+ * nothing else; the implicit arms in AccessService (authorship, chat
10
+ * membership, agreement party, org membership) leave no row behind, so a reader
11
+ * can be entitled to something this list will never mention. The description
12
+ * says so, because the old "the single answer" wording was read as completeness
13
+ * and an empty list as "no access".
7
14
  */
8
15
  export declare const listGrantsCapability: CapabilityDefinition;
9
16
  export declare const GRANTS_CAPABILITIES: CapabilityDefinition[];
@@ -1,12 +1,11 @@
1
1
  import { GrantsClient } from '../http/GrantsClient.js';
2
+ import { GRANT_SCOPE_KINDS, } from '../http/grants.js';
2
3
  import { fullCreds } from './types.js';
3
- const SCOPE_KINDS = [
4
- 'chat',
5
- 'agreement',
6
- 'org',
7
- 'connection',
8
- 'wallet',
9
- ];
4
+ // BOTH the tool's param enum and its validator, so a missing kind makes the
5
+ // filter the description advertises fail validation. Derived from the canonical
6
+ // list next to the type rather than restated, because that is exactly how
7
+ // `artifact` came to be advertised but not accepted.
8
+ const SCOPE_KINDS = GRANT_SCOPE_KINDS;
10
9
  const HEALTHS = ['active', 'expired', 'revoked'];
11
10
  function parseScopeKinds(raw) {
12
11
  if (raw == null)
@@ -24,23 +23,30 @@ function parseScopeKinds(raw) {
24
23
  }
25
24
  /**
26
25
  * ZIG-893 — the single "what authority do I hold?" tool. One name, every rail
27
- * (context chat/agreement/org, connection, wallet), holder-scoped,
26
+ * (context chat/agreement/org/artifact, connection, wallet), holder-scoped,
28
27
  * cross-session. `unreadableRails` comes from the backend (ZIG-956) so a short
29
28
  * list is never presented as complete when the key can't read a rail.
29
+ *
30
+ * ZIG-1088 — HOLD, not reach. `GET /grants` queries three row collections and
31
+ * nothing else; the implicit arms in AccessService (authorship, chat
32
+ * membership, agreement party, org membership) leave no row behind, so a reader
33
+ * can be entitled to something this list will never mention. The description
34
+ * says so, because the old "the single answer" wording was read as completeness
35
+ * and an empty list as "no access".
30
36
  */
31
37
  export const listGrantsCapability = {
32
38
  key: 'grant_list',
33
39
  names: { sdk: 'grant_list', mcp: 'ziggs_grant_list' },
34
40
  descriptions: {
35
- sdk: 'List every grant this agent holds across all rails in one call — context (chat/agreement/org), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no message/artifact content or credentials). The single answer to "what grants of mine do you hold?", holder-scoped and cross-session. Filter by scopeKind (rail) and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails, not silently dropped. Cursor-paginated: pass cursor from a prior nextCursor. Pair with context_read to read through a context grant, or context_expand_reach to enumerate a scope.',
36
- mcp: 'List every grant this delegate holds across all rails in one call — context (chat/agreement/org), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). The single answer to "what grants of mine do you hold?", holder-scoped and cross-session. Filter by scopeKind (rail) and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails, not silently dropped. Cursor-paginated: pass cursor from a prior nextCursor to page. Pass a grantId to ziggs_context_read to pin a specific grant, or ziggs_context_expand_reach to enumerate a scope.',
41
+ sdk: 'List every grant this agent holds across all rails in one call — context (chat/agreement/org/artifact), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no message/artifact content or credentials). Holder-scoped and cross-session. It answers "what grants do I HOLD?", which is narrower than "what can I reach?": reach you have by authoring something, by sitting in a chat, by being a party to an agreement, or through your org is not a grant row and never appears here — so an empty list means "no grants", never "no access". Filter by scopeKind (rail) and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails, not silently dropped. Cursor-paginated: pass cursor from a prior nextCursor. To answer "what can I reach?" instead, use context_expand_reach to enumerate a scope, or context_read to read through a grant.',
42
+ mcp: 'List every grant this delegate holds across all rails in one call — context (chat/agreement/org/artifact), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). Holder-scoped and cross-session. It answers "what grants do I HOLD?", which is narrower than "what can I reach?": reach you have by authoring something, by sitting in a chat, by being a party to an agreement, or through your org is not a grant row and never appears here — so an empty list means "no grants", never "no access". Filter by scopeKind (rail) and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails, not silently dropped. Cursor-paginated: pass cursor from a prior nextCursor to page. To answer "what can I reach?" instead, use ziggs_context_expand_reach to enumerate a scope, or pass a grantId to ziggs_context_read to pin a specific grant.',
37
43
  },
38
44
  annotation: 'read-only',
39
45
  params: {
40
46
  scopeKind: {
41
47
  type: 'array',
42
48
  items: { type: 'string', enum: SCOPE_KINDS },
43
- description: 'Rail filter (repeatable): chat | agreement | org (context) | connection | wallet. Omit for every rail you can read.',
49
+ description: 'Rail filter (repeatable): chat | agreement | org | artifact (context) | connection | wallet. Omit for every rail you can read.',
44
50
  },
45
51
  health: {
46
52
  type: 'string',
@@ -7,5 +7,5 @@ export { GRANTS_CAPABILITIES, listGrantsCapability } from './grants.js';
7
7
  export { CONTEXT_CAPABILITIES, contextReadCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, contextBounds, resolveOrgScopeId, } from './context.js';
8
8
  export { CONNECTION_CAPABILITIES, connectionProxyCapability, requestConnectionCapability, } from './connections.js';
9
9
  export { DISCOVERY_CAPABILITIES, agentSearchCapability, agentGetCapability, } from './discovery.js';
10
- export { ARTIFACT_CAPABILITIES, recordArtifactCapability } from './artifacts.js';
10
+ export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, shareArtifactCapability, attachArtifactCapability, } from './artifacts.js';
11
11
  export { CHAT_CAPABILITIES, openConversationCapability } from './chat.js';
@@ -7,5 +7,5 @@ export { GRANTS_CAPABILITIES, listGrantsCapability } from './grants.js';
7
7
  export { CONTEXT_CAPABILITIES, contextReadCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, contextBounds, resolveOrgScopeId, } from './context.js';
8
8
  export { CONNECTION_CAPABILITIES, connectionProxyCapability, requestConnectionCapability, } from './connections.js';
9
9
  export { DISCOVERY_CAPABILITIES, agentSearchCapability, agentGetCapability, } from './discovery.js';
10
- export { ARTIFACT_CAPABILITIES, recordArtifactCapability } from './artifacts.js';
10
+ export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, shareArtifactCapability, attachArtifactCapability, } from './artifacts.js';
11
11
  export { CHAT_CAPABILITIES, openConversationCapability } from './chat.js';
@@ -43,6 +43,7 @@ export interface CapabilityEnv {
43
43
  creds: {
44
44
  operatorKey: string;
45
45
  agentId?: string;
46
+ laneId?: string;
46
47
  };
47
48
  /** SDK runner base-URL override (BACKEND_URL / ZIGGS_BACKEND_URL). */
48
49
  baseUrl?: string;
@@ -1,11 +1,13 @@
1
1
  /** Creds for impersonated calls; adapters guarantee agentId when needsAgentId. */
2
2
  export function fullCreds(env) {
3
- const { operatorKey, agentId } = env.creds;
3
+ const { operatorKey, agentId, laneId } = env.creds;
4
4
  if (!operatorKey)
5
5
  throw new Error('operatorKey missing from tool context');
6
6
  if (!agentId)
7
7
  throw new Error('agentId missing from tool context');
8
- return { operatorKey, agentId };
8
+ // ZIG-1092 — the lane rides along so every Creds-based client sends
9
+ // X-Ziggs-Lane without each capability having to remember to.
10
+ return { operatorKey, agentId, ...(laneId ? { laneId } : {}) };
9
11
  }
10
12
  /**
11
13
  * Re-throw a client error with a capability-level prefix, preserving the HTTP