@ziggs-ai/api-client 0.7.1 → 0.8.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.
- package/dist/capabilities/artifacts.d.ts +28 -0
- package/dist/capabilities/artifacts.js +194 -12
- package/dist/capabilities/context.js +11 -8
- package/dist/capabilities/grants.d.ts +1 -1
- package/dist/capabilities/grants.js +10 -11
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/capabilities/index.js +1 -1
- package/dist/http/AgreementClient.js +19 -23
- package/dist/http/ArtifactsClient.d.ts +33 -0
- package/dist/http/ArtifactsClient.js +54 -6
- package/dist/http/ChatClient.js +1 -14
- package/dist/http/ContextGrantsClient.d.ts +21 -1
- package/dist/http/ContextGrantsClient.js +36 -18
- package/dist/http/MarketplaceClient.js +1 -10
- package/dist/http/PaymentsClient.js +2 -12
- package/dist/http/TaskClient.js +1 -21
- package/dist/http/grants.d.ts +12 -1
- package/dist/http/grants.js +21 -0
- package/dist/http/index.d.ts +1 -1
- package/dist/http/index.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/shared/apiError.d.ts +22 -0
- package/dist/shared/apiError.js +57 -0
- package/package.json +1 -1
|
@@ -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
|
|
24
|
-
|
|
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 =
|
|
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: '
|
|
45
|
+
chatId: { type: 'string', description: 'Optional chat scope' },
|
|
38
46
|
agreementId: {
|
|
39
47
|
type: 'string',
|
|
40
|
-
description: '
|
|
48
|
+
description: 'Optional agreement scope — for a hire deliverable, prefer this. When both chatId and agreementId are passed, agreementId wins.',
|
|
41
49
|
},
|
|
42
50
|
taskId: {
|
|
43
51
|
type: 'string',
|
|
44
|
-
description: 'Optional task —
|
|
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',
|
|
@@ -58,12 +66,13 @@ export const recordArtifactCapability = {
|
|
|
58
66
|
// Agreement wins when both scopes arrive. Models naturally pass the chat
|
|
59
67
|
// they are standing in alongside the agreement they deliver under; a hard
|
|
60
68
|
// xor here failed the deliverable at the last step of a finished task
|
|
61
|
-
// (ZIG-924 dogfood). The backend
|
|
62
|
-
//
|
|
69
|
+
// (ZIG-924 dogfood). The backend accepts at most one — we resolve the
|
|
70
|
+
// ambiguity at the exposed surface instead of erroring.
|
|
63
71
|
const chatId = agreementId ? undefined : args['chatId'];
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
// ZIG-1037: no scope is legal. The throw that used to live here ("Pass
|
|
73
|
+
// chatId or agreementId") is the exact failure this ticket removed — it cost
|
|
74
|
+
// a live agent a turn mid-delivery for naming no container, when the record
|
|
75
|
+
// itself never needed one.
|
|
67
76
|
const visibility = args['visibility'];
|
|
68
77
|
if (visibility !== 'chat' && visibility !== 'agent-private') {
|
|
69
78
|
throw new Error('visibility must be chat or agent-private');
|
|
@@ -91,4 +100,177 @@ export const recordArtifactCapability = {
|
|
|
91
100
|
};
|
|
92
101
|
},
|
|
93
102
|
};
|
|
94
|
-
|
|
103
|
+
/**
|
|
104
|
+
* ZIG-1037 — the artifacts you wrote, across every scope and none.
|
|
105
|
+
*
|
|
106
|
+
* The scope listings (`context_read` via chat / agreement / task) read attach
|
|
107
|
+
* tables, so a free-standing artifact appears in none of them. This is how an
|
|
108
|
+
* agent finds what it recorded without naming a container.
|
|
109
|
+
*/
|
|
110
|
+
export const listArtifactsCapability = {
|
|
111
|
+
key: 'artifact_list',
|
|
112
|
+
names: { sdk: 'artifact_list', mcp: 'ziggs_artifact_list' },
|
|
113
|
+
descriptions: {
|
|
114
|
+
sdk: 'List artifacts you authored, in any scope or none — including free-standing ones no scope read can reach. Forward-delta with `after`.',
|
|
115
|
+
mcp: 'List artifacts YOU authored, across every scope and none. Use this to find something you ' +
|
|
116
|
+
'recorded free-standing (no chat/agreement/task), which the scope reads cannot return. ' +
|
|
117
|
+
'To read an artifact someone shared WITH you, use ziggs_context_read with via=artifact:<id>; ' +
|
|
118
|
+
'to see what has been shared with you, use ziggs_grant_list with scopeKind=artifact.',
|
|
119
|
+
},
|
|
120
|
+
annotation: 'read-only',
|
|
121
|
+
params: {
|
|
122
|
+
after: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
description: 'ISO timestamp — return only artifacts written strictly after this',
|
|
125
|
+
},
|
|
126
|
+
limit: { type: 'number', description: 'Page size (server default when omitted)' },
|
|
127
|
+
},
|
|
128
|
+
needsAgentId: true,
|
|
129
|
+
handler: async (args, env) => {
|
|
130
|
+
const creds = fullCreds(env);
|
|
131
|
+
return new ArtifactsClient(creds.operatorKey, creds.agentId).list({ authoredBy: 'me' }, {
|
|
132
|
+
after: args['after'],
|
|
133
|
+
limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* ZIG-1037 — hand one artifact you authored to one other agent.
|
|
139
|
+
*
|
|
140
|
+
* Deliberately not part of `context_delegate`: there is no parent grant to
|
|
141
|
+
* attenuate here. An agent holds no grant over its own output — authorship is
|
|
142
|
+
* the authority, and it belongs to the agent's owner, which is why a receiver
|
|
143
|
+
* outside that owner's trust boundary needs their approval. What counts as
|
|
144
|
+
* "inside" is the shared new-party policy, not a local rule: same owner, same
|
|
145
|
+
* org, or an active agent link.
|
|
146
|
+
*/
|
|
147
|
+
export const shareArtifactCapability = {
|
|
148
|
+
key: 'artifact_share',
|
|
149
|
+
names: { sdk: 'artifact_share', mcp: 'ziggs_artifact_share' },
|
|
150
|
+
descriptions: {
|
|
151
|
+
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.',
|
|
152
|
+
mcp: 'Share ONE artifact you authored with another agent, without sharing any chat or agreement ' +
|
|
153
|
+
'it sits in. Only the authoring agent can call this. A receiver already connected to you — ' +
|
|
154
|
+
'same owner, same org, or an active link — is granted immediately. Any other agent is ' +
|
|
155
|
+
'offered to your owner to approve, and the reply is { status: "pending_approval", ' +
|
|
156
|
+
'agreementId } — nothing is shared until they do. Pass chatId to have that approval appear ' +
|
|
157
|
+
'in the chat you are working in. The receiver then reads it with ziggs_context_read ' +
|
|
158
|
+
'via=artifact:<id>.',
|
|
159
|
+
},
|
|
160
|
+
annotation: 'write',
|
|
161
|
+
params: {
|
|
162
|
+
artifactId: {
|
|
163
|
+
type: 'string',
|
|
164
|
+
required: true,
|
|
165
|
+
description: 'The artifact to share — you must be its author',
|
|
166
|
+
},
|
|
167
|
+
holderId: { type: 'string', required: true, description: 'Agent id that receives it' },
|
|
168
|
+
expiresAt: {
|
|
169
|
+
type: 'string',
|
|
170
|
+
description: 'ISO-8601 expiry; omit for the default grant lifetime',
|
|
171
|
+
},
|
|
172
|
+
chatId: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
description: 'Chat to surface the approval request in, when the receiver’s owner has to approve',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
needsAgentId: true,
|
|
178
|
+
handler: async (args, env) => {
|
|
179
|
+
const creds = fullCreds(env);
|
|
180
|
+
const artifactId = args['artifactId'];
|
|
181
|
+
if (!artifactId)
|
|
182
|
+
throw new Error('artifactId is required');
|
|
183
|
+
const holderId = args['holderId'];
|
|
184
|
+
if (!holderId)
|
|
185
|
+
throw new Error('holderId is required');
|
|
186
|
+
const result = await new ContextGrantsClient(creds.operatorKey, creds.agentId, env.baseUrl).shareArtifact(artifactId, {
|
|
187
|
+
holderId,
|
|
188
|
+
expiresAt: args['expiresAt'],
|
|
189
|
+
chatId: args['chatId'],
|
|
190
|
+
});
|
|
191
|
+
if (result.status === 'pending_approval') {
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
...result,
|
|
195
|
+
note: `Not shared yet — ${result.ownerId ?? 'the owner'} must approve agreement ${result.agreementId}.`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
ok: true,
|
|
200
|
+
...result,
|
|
201
|
+
note: `${holderId} can now read artifact ${artifactId} with via=artifact:${artifactId}.`,
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* ZIG-1037 — attach an artifact you already have to a chat or a task.
|
|
207
|
+
*
|
|
208
|
+
* The other half of "record now, decide where later". Attaching confers nothing
|
|
209
|
+
* by itself: it puts the artifact inside the container, and that container's
|
|
210
|
+
* audience can read it from then on. Use this to publish to a room or bind a
|
|
211
|
+
* deliverable to a task; use artifact_share to hand it to ONE agent instead.
|
|
212
|
+
*/
|
|
213
|
+
export const attachArtifactCapability = {
|
|
214
|
+
key: 'artifact_attach',
|
|
215
|
+
names: { sdk: 'artifact_attach', mcp: 'ziggs_artifact_attach' },
|
|
216
|
+
descriptions: {
|
|
217
|
+
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.',
|
|
218
|
+
mcp: 'Attach an existing artifact to a chat or a task — the way a free-standing artifact (one you ' +
|
|
219
|
+
'recorded with no scope) reaches an audience. Pass exactly one of chatId or taskId. Everyone ' +
|
|
220
|
+
'in that chat, or party to that task, can read it from then on; attaching gives you nothing ' +
|
|
221
|
+
'new yourself. You must already be able to read the artifact AND belong to the container. ' +
|
|
222
|
+
'To hand it to ONE specific agent without opening a chat, use ziggs_artifact_share instead.',
|
|
223
|
+
},
|
|
224
|
+
annotation: 'write',
|
|
225
|
+
params: {
|
|
226
|
+
artifactId: { type: 'string', required: true, description: 'Artifact to attach' },
|
|
227
|
+
chatId: { type: 'string', description: 'Chat to attach it to' },
|
|
228
|
+
taskId: { type: 'string', description: 'Task to attach it to' },
|
|
229
|
+
role: {
|
|
230
|
+
type: 'string',
|
|
231
|
+
enum: ['input', 'output'],
|
|
232
|
+
description: 'Task attach only: output (a deliverable, default) or input (something the task consumes). ' +
|
|
233
|
+
'A link’s role is set once — re-attaching with a different role is refused, not changed.',
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
needsAgentId: true,
|
|
237
|
+
handler: async (args, env) => {
|
|
238
|
+
const artifactId = args['artifactId'];
|
|
239
|
+
if (!artifactId)
|
|
240
|
+
throw new Error('artifactId is required');
|
|
241
|
+
const chatId = args['chatId'];
|
|
242
|
+
const taskId = args['taskId'];
|
|
243
|
+
if (!!chatId === !!taskId) {
|
|
244
|
+
throw new Error('Pass exactly one of chatId or taskId');
|
|
245
|
+
}
|
|
246
|
+
const role = args['role'] ?? 'output';
|
|
247
|
+
if (role !== 'input' && role !== 'output') {
|
|
248
|
+
throw new Error('role must be input or output');
|
|
249
|
+
}
|
|
250
|
+
const creds = fullCreds(env);
|
|
251
|
+
const client = new ArtifactsClient(creds.operatorKey, creds.agentId);
|
|
252
|
+
if (chatId) {
|
|
253
|
+
await client.attachToChat(artifactId, chatId);
|
|
254
|
+
return {
|
|
255
|
+
ok: true,
|
|
256
|
+
artifactId,
|
|
257
|
+
chatId,
|
|
258
|
+
note: `Everyone in chat ${chatId} can now read artifact ${artifactId}.`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
await client.attachToTask(artifactId, taskId, role);
|
|
262
|
+
return {
|
|
263
|
+
ok: true,
|
|
264
|
+
artifactId,
|
|
265
|
+
taskId,
|
|
266
|
+
role,
|
|
267
|
+
note: `Artifact ${artifactId} is attached to task ${taskId} as ${role}; the task's parties can read it.`,
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
export const ARTIFACT_CAPABILITIES = [
|
|
272
|
+
recordArtifactCapability,
|
|
273
|
+
listArtifactsCapability,
|
|
274
|
+
shareArtifactCapability,
|
|
275
|
+
attachArtifactCapability,
|
|
276
|
+
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ContextReadClient } 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
7
|
const CONTEXT_READ_TYPES = [
|
|
@@ -10,7 +10,10 @@ const CONTEXT_READ_TYPES = [
|
|
|
10
10
|
'agreements',
|
|
11
11
|
'tasks',
|
|
12
12
|
];
|
|
13
|
-
|
|
13
|
+
// ZIG-1037: `artifact` joined the context rail. Delegating an artifact grant
|
|
14
|
+
// onward works (same kind, same id — an exact re-grant); narrowing a container
|
|
15
|
+
// grant DOWN to an artifact inside it is deliberately not supported yet.
|
|
16
|
+
const GRANT_SCOPE_KINDS = CONTEXT_GRANT_SCOPE_KINDS;
|
|
14
17
|
const CONTEXT_TEMPORALS = ['from-now', 'from-start'];
|
|
15
18
|
/**
|
|
16
19
|
* Human/LLM-readable bounds for a context grant. ZIG-646 folded a context
|
|
@@ -50,8 +53,8 @@ export const contextReadCapability = {
|
|
|
50
53
|
key: 'context_read',
|
|
51
54
|
names: { sdk: 'context_read', mcp: 'ziggs_context_read' },
|
|
52
55
|
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
|
|
56
|
+
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. For artifacts you can also name one directly with via=artifact:<id> — the only way to read an artifact attached to no chat, agreement or task. Cursored; all access is grant-fenced server-side.',
|
|
57
|
+
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> — plus via=artifact:<id> for artifacts, a point read of one named artifact and the ONLY way to read one that is attached to no container (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
58
|
},
|
|
56
59
|
annotation: 'read-only',
|
|
57
60
|
params: {
|
|
@@ -64,7 +67,7 @@ export const contextReadCapability = {
|
|
|
64
67
|
via: {
|
|
65
68
|
type: 'string',
|
|
66
69
|
required: true,
|
|
67
|
-
description: 'Scope entry you hold, e.g. chat:<id>, agreement:<id>, task:<id>',
|
|
70
|
+
description: 'Scope entry you hold, e.g. chat:<id>, agreement:<id>, task:<id>. For type=artifacts also artifact:<id> to read that one artifact.',
|
|
68
71
|
},
|
|
69
72
|
cursor: { type: 'string', description: 'Opaque cursor from a prior nextCursor to page' },
|
|
70
73
|
after: { type: 'string', description: 'ISO timestamp for forward-delta (messages/artifacts)' },
|
|
@@ -109,8 +112,8 @@ export const contextExpandReachCapability = {
|
|
|
109
112
|
key: 'context_expand_reach',
|
|
110
113
|
names: { sdk: 'context_expand_reach', mcp: 'ziggs_context_expand_reach' },
|
|
111
114
|
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.',
|
|
115
|
+
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.',
|
|
116
|
+
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
117
|
},
|
|
115
118
|
annotation: 'read-only',
|
|
116
119
|
params: {
|
|
@@ -179,7 +182,7 @@ export const contextDelegateCapability = {
|
|
|
179
182
|
}
|
|
180
183
|
const scopeKind = args['scopeKind'];
|
|
181
184
|
if (!GRANT_SCOPE_KINDS.includes(scopeKind)) {
|
|
182
|
-
throw new Error(
|
|
185
|
+
throw new Error(`scopeKind must be one of ${GRANT_SCOPE_KINDS.join(', ')}`);
|
|
183
186
|
}
|
|
184
187
|
// ZIG-941 #6 — an org scope may be named rather than pasted as org_… id.
|
|
185
188
|
let scopeId = args['scopeId'];
|
|
@@ -1,7 +1,7 @@
|
|
|
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
7
|
*/
|
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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,7 +23,7 @@ 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.
|
|
30
29
|
*/
|
|
@@ -32,15 +31,15 @@ export const listGrantsCapability = {
|
|
|
32
31
|
key: 'grant_list',
|
|
33
32
|
names: { sdk: 'grant_list', mcp: 'ziggs_grant_list' },
|
|
34
33
|
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.',
|
|
34
|
+
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). 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.',
|
|
35
|
+
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). 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.',
|
|
37
36
|
},
|
|
38
37
|
annotation: 'read-only',
|
|
39
38
|
params: {
|
|
40
39
|
scopeKind: {
|
|
41
40
|
type: 'array',
|
|
42
41
|
items: { type: 'string', enum: SCOPE_KINDS },
|
|
43
|
-
description: 'Rail filter (repeatable): chat | agreement | org (context) | connection | wallet. Omit for every rail you can read.',
|
|
42
|
+
description: 'Rail filter (repeatable): chat | agreement | org | artifact (context) | connection | wallet. Omit for every rail you can read.',
|
|
44
43
|
},
|
|
45
44
|
health: {
|
|
46
45
|
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';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
3
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
4
|
-
import { OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, isBroadcastTarget
|
|
4
|
+
import { OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, isBroadcastTarget } from '../types.js';
|
|
5
|
+
import { throwApiError } from '../shared/apiError.js';
|
|
5
6
|
// Lazy: read at call time so dotenv loaded after this module is imported
|
|
6
7
|
// still takes effect. Baking it at module-load time would freeze the URL
|
|
7
8
|
// before the caller's dotenv.config() runs.
|
|
@@ -21,20 +22,6 @@ function assertCreds(creds, op) {
|
|
|
21
22
|
if (!creds?.agentId)
|
|
22
23
|
throw new Error(`agentId is required for ${op}`);
|
|
23
24
|
}
|
|
24
|
-
function parseErrorMessage(responseBody, defaultMessage) {
|
|
25
|
-
if (!responseBody)
|
|
26
|
-
return defaultMessage;
|
|
27
|
-
try {
|
|
28
|
-
const d = JSON.parse(responseBody);
|
|
29
|
-
return d['details'] || d['error'] || d['message'] || defaultMessage;
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return responseBody || defaultMessage;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function throwApiError(response, responseBody, defaultMessage) {
|
|
36
|
-
throw new ApiError(parseErrorMessage(responseBody, defaultMessage), response.status, responseBody);
|
|
37
|
-
}
|
|
38
25
|
export async function proposeAgreement(proposalData, creds) {
|
|
39
26
|
if (!proposalData)
|
|
40
27
|
throw new Error('Proposal data is required for proposal creation');
|
|
@@ -124,19 +111,28 @@ export async function respondToAgreement(agreementId, action, creds, opts = {})
|
|
|
124
111
|
if (!agreement) {
|
|
125
112
|
throw new Error(`Agreement ${agreementId} not found`);
|
|
126
113
|
}
|
|
114
|
+
const partyId = resolveMyPendingApprovalPartyId(agreement, {
|
|
115
|
+
ownerUserId: opts.ownerUserId,
|
|
116
|
+
agentId: creds.agentId,
|
|
117
|
+
});
|
|
127
118
|
const proposedTo = agreement.parties?.proposedTo;
|
|
128
119
|
if (isBroadcastTarget(proposedTo) || isBroadcastTarget(agreement.parties?.payer)) {
|
|
129
120
|
// ZIG-1021: respond is approve/reject on DIRECT proposals only. An open
|
|
130
121
|
// broadcast (quest, standing offer, link invite) has no per-recipient
|
|
131
|
-
// approval slot —
|
|
132
|
-
// (ZIG-719);
|
|
133
|
-
|
|
122
|
+
// approval slot — a BYSTANDER can neither approve nor reject it
|
|
123
|
+
// (ZIG-719); their move is to claim it, which has its own verb.
|
|
124
|
+
//
|
|
125
|
+
// ZIG-1077: with one exception — the caller who HOLDS a pending approval
|
|
126
|
+
// on the row. A hand-off pins its provider and that provider's consent
|
|
127
|
+
// is a real ledger slot even on an open broadcast (ZIG-1059); this guard
|
|
128
|
+
// used to reject before looking, so the provider could not consent
|
|
129
|
+
// through ANY surface and the hand-off sat unclaimable forever. The
|
|
130
|
+
// backend keeps a consented broadcast OPEN for claims.
|
|
131
|
+
if (!partyId) {
|
|
132
|
+
throw new Error(`Agreement ${agreementId} is an open broadcast and has no personal approval slot — respond cannot ${action} it. Claim it with the claim tool (agreement_claim / ziggs_agreement_claim), or ignore it to pass.`);
|
|
133
|
+
}
|
|
134
134
|
}
|
|
135
|
-
|
|
136
|
-
ownerUserId: opts.ownerUserId,
|
|
137
|
-
agentId: creds.agentId,
|
|
138
|
-
});
|
|
139
|
-
if (!partyId) {
|
|
135
|
+
else if (!partyId) {
|
|
140
136
|
throw new Error(`No pending approval entry for this operator on agreement ${agreementId}`);
|
|
141
137
|
}
|
|
142
138
|
return approveAgreementAsParty(agreementId, partyId, action === 'approve' ? 'approved' : 'rejected', creds);
|
|
@@ -7,6 +7,13 @@ export interface ListArtifactsOptions {
|
|
|
7
7
|
export interface ListArtifactsQuery {
|
|
8
8
|
chatId?: string;
|
|
9
9
|
agreementId?: string;
|
|
10
|
+
taskId?: string;
|
|
11
|
+
/**
|
|
12
|
+
* ZIG-1037 — `'me'` lists artifacts YOU authored, in any scope or none. The
|
|
13
|
+
* only listing that surfaces a free-standing artifact, since the others read
|
|
14
|
+
* attach tables. Mutually exclusive with the scope filters.
|
|
15
|
+
*/
|
|
16
|
+
authoredBy?: 'me';
|
|
10
17
|
}
|
|
11
18
|
export interface ListArtifactsResult {
|
|
12
19
|
artifacts: unknown[];
|
|
@@ -84,6 +91,32 @@ export declare class ArtifactsClient {
|
|
|
84
91
|
writeStrict(input: WriteArtifactInput): Promise<{
|
|
85
92
|
artifactId?: string;
|
|
86
93
|
}>;
|
|
94
|
+
/**
|
|
95
|
+
* ZIG-1037 — attach an existing artifact to a chat or a task.
|
|
96
|
+
*
|
|
97
|
+
* Attaching confers nothing on its own: it places the artifact inside the
|
|
98
|
+
* container, and that container's audience (chat members / task parties) can
|
|
99
|
+
* read it from then on. This is how a free-standing artifact reaches anyone
|
|
100
|
+
* without granting it to one specific agent.
|
|
101
|
+
*
|
|
102
|
+
* The agreement equivalent already existed as POST /agreements/:id/artifacts.
|
|
103
|
+
*/
|
|
104
|
+
attachToChat(artifactId: string, chatId: string): Promise<{
|
|
105
|
+
success: boolean;
|
|
106
|
+
}>;
|
|
107
|
+
attachToTask(artifactId: string, taskId: string, role?: 'input' | 'output'): Promise<{
|
|
108
|
+
success: boolean;
|
|
109
|
+
}>;
|
|
110
|
+
private _attach;
|
|
111
|
+
/**
|
|
112
|
+
* ZIG-1037 — at most one container, and none is fine.
|
|
113
|
+
*
|
|
114
|
+
* This used to demand exactly one, which is what made a deliverable fail at the
|
|
115
|
+
* last step when the model had not worked out which container it was standing
|
|
116
|
+
* in. A write with no scope is now a free-standing artifact; `taskId` alone
|
|
117
|
+
* binds it to a task. Both containers at once is still a mistake worth naming,
|
|
118
|
+
* since only one of them would take effect.
|
|
119
|
+
*/
|
|
87
120
|
private _assertScopeXor;
|
|
88
121
|
private _headers;
|
|
89
122
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
|
+
import { throwApiError } from '../shared/apiError.js';
|
|
3
4
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
4
5
|
import { buildOperatorHeaders } from './operatorHeaders.js';
|
|
5
6
|
/**
|
|
@@ -38,14 +39,19 @@ export class ArtifactsClient {
|
|
|
38
39
|
this.agentId = agentId;
|
|
39
40
|
}
|
|
40
41
|
async list(q, opts = {}) {
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
const selectors = [q.chatId, q.agreementId, q.taskId, q.authoredBy].filter(Boolean);
|
|
43
|
+
if (selectors.length !== 1) {
|
|
44
|
+
throw new Error('ArtifactsClient.list: pass exactly one of chatId, agreementId, taskId, or authoredBy');
|
|
43
45
|
}
|
|
44
46
|
const url = new URL(`${getBackendUrl()}/artifacts`);
|
|
45
47
|
if (q.chatId)
|
|
46
48
|
url.searchParams.set('chatId', q.chatId);
|
|
47
49
|
if (q.agreementId)
|
|
48
50
|
url.searchParams.set('agreementId', q.agreementId);
|
|
51
|
+
if (q.taskId)
|
|
52
|
+
url.searchParams.set('taskId', q.taskId);
|
|
53
|
+
if (q.authoredBy)
|
|
54
|
+
url.searchParams.set('authoredBy', q.authoredBy);
|
|
49
55
|
if (opts.after)
|
|
50
56
|
url.searchParams.set('after', opts.after);
|
|
51
57
|
if (opts.limit != null)
|
|
@@ -53,7 +59,7 @@ export class ArtifactsClient {
|
|
|
53
59
|
const res = await fetch(url.toString(), { headers: this._headers() });
|
|
54
60
|
if (!res.ok) {
|
|
55
61
|
const body = await res.text().catch(() => '');
|
|
56
|
-
|
|
62
|
+
throwApiError(res, body, `listArtifacts failed: ${res.status} ${res.statusText}`);
|
|
57
63
|
}
|
|
58
64
|
return (await res.json());
|
|
59
65
|
}
|
|
@@ -118,7 +124,7 @@ export class ArtifactsClient {
|
|
|
118
124
|
});
|
|
119
125
|
const body = await res.text().catch(() => '');
|
|
120
126
|
if (!res.ok) {
|
|
121
|
-
|
|
127
|
+
throwApiError(res, body, `recordArtifact failed: ${res.status} ${res.statusText}`);
|
|
122
128
|
}
|
|
123
129
|
try {
|
|
124
130
|
const parsed = body ? JSON.parse(body) : {};
|
|
@@ -128,9 +134,51 @@ export class ArtifactsClient {
|
|
|
128
134
|
return {};
|
|
129
135
|
}
|
|
130
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* ZIG-1037 — attach an existing artifact to a chat or a task.
|
|
139
|
+
*
|
|
140
|
+
* Attaching confers nothing on its own: it places the artifact inside the
|
|
141
|
+
* container, and that container's audience (chat members / task parties) can
|
|
142
|
+
* read it from then on. This is how a free-standing artifact reaches anyone
|
|
143
|
+
* without granting it to one specific agent.
|
|
144
|
+
*
|
|
145
|
+
* The agreement equivalent already existed as POST /agreements/:id/artifacts.
|
|
146
|
+
*/
|
|
147
|
+
async attachToChat(artifactId, chatId) {
|
|
148
|
+
return this._attach(`/chats/${encodeURIComponent(chatId)}/artifacts`, {
|
|
149
|
+
artifactId,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async attachToTask(artifactId, taskId, role = 'output') {
|
|
153
|
+
return this._attach(`/tasks/${encodeURIComponent(taskId)}/artifacts`, {
|
|
154
|
+
artifactId,
|
|
155
|
+
role,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async _attach(path, body) {
|
|
159
|
+
const res = await fetch(`${getBackendUrl()}${path}`, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: this._headers(),
|
|
162
|
+
body: JSON.stringify(body),
|
|
163
|
+
});
|
|
164
|
+
const text = await res.text().catch(() => '');
|
|
165
|
+
if (!res.ok) {
|
|
166
|
+
throwApiError(res, text, `attachArtifact failed: ${res.status} ${res.statusText}`);
|
|
167
|
+
}
|
|
168
|
+
return { success: true };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* ZIG-1037 — at most one container, and none is fine.
|
|
172
|
+
*
|
|
173
|
+
* This used to demand exactly one, which is what made a deliverable fail at the
|
|
174
|
+
* last step when the model had not worked out which container it was standing
|
|
175
|
+
* in. A write with no scope is now a free-standing artifact; `taskId` alone
|
|
176
|
+
* binds it to a task. Both containers at once is still a mistake worth naming,
|
|
177
|
+
* since only one of them would take effect.
|
|
178
|
+
*/
|
|
131
179
|
_assertScopeXor(input) {
|
|
132
|
-
if (
|
|
133
|
-
throw new Error('ArtifactsClient.write: pass
|
|
180
|
+
if (input.chatId && input.agreementId) {
|
|
181
|
+
throw new Error('ArtifactsClient.write: pass at most one of chatId or agreementId');
|
|
134
182
|
}
|
|
135
183
|
}
|
|
136
184
|
_headers() {
|
package/dist/http/ChatClient.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
3
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
4
|
-
import {
|
|
4
|
+
import { throwApiError } from '../shared/apiError.js';
|
|
5
5
|
function buildHeaders(creds) {
|
|
6
6
|
return {
|
|
7
7
|
'content-type': 'application/json',
|
|
@@ -15,19 +15,6 @@ function assertCreds(creds, op) {
|
|
|
15
15
|
if (!creds?.agentId)
|
|
16
16
|
throw new Error(`agentId is required for ${op}`);
|
|
17
17
|
}
|
|
18
|
-
function throwApiError(response, responseBody, defaultMessage) {
|
|
19
|
-
let message = defaultMessage;
|
|
20
|
-
if (responseBody) {
|
|
21
|
-
try {
|
|
22
|
-
const d = JSON.parse(responseBody);
|
|
23
|
-
message = d['details'] || d['error'] || d['message'] || defaultMessage;
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
message = responseBody || defaultMessage;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
throw new ApiError(message, response.status, responseBody);
|
|
30
|
-
}
|
|
31
18
|
export async function openConversation(participantId, creds) {
|
|
32
19
|
if (!participantId)
|
|
33
20
|
throw new Error('participantId is required for openConversation');
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import type { GrantView } from './grants.js';
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* ZIG-1037 added `artifact` — the narrowest context scope: one specific artifact,
|
|
5
|
+
* shared without sharing any chat or agreement it sits in. Still the context
|
|
6
|
+
* rail, not a new grant primitive.
|
|
7
|
+
*/
|
|
8
|
+
export type ContextGrantScopeKind = 'chat' | 'agreement' | 'org' | 'artifact';
|
|
4
9
|
export type ContextTemporal = 'from-now' | 'from-start';
|
|
5
10
|
export interface ContextGrantScope {
|
|
6
11
|
kind: ContextGrantScopeKind;
|
|
@@ -80,6 +85,21 @@ export declare class ContextGrantsClient {
|
|
|
80
85
|
* labels-only, grant-fenced server-side.
|
|
81
86
|
*/
|
|
82
87
|
getReach(grantId: string): Promise<GrantReachResult>;
|
|
88
|
+
/**
|
|
89
|
+
* ZIG-1037 — share an artifact THIS agent authored with another agent.
|
|
90
|
+
*
|
|
91
|
+
* Not a delegation: an agent holds no grant over its own output, so there is
|
|
92
|
+
* no parent to attenuate. Authorship is the authority, held by the agent's
|
|
93
|
+
* owner — which is why the receiver decides the outcome. A receiver already
|
|
94
|
+
* inside the sharer's trust boundary (same owner, same org, or an active link)
|
|
95
|
+
* is granted immediately; anyone else opens a request the owner approves,
|
|
96
|
+
* exactly like `delegateGrant`'s new-party path.
|
|
97
|
+
*/
|
|
98
|
+
shareArtifact(artifactId: string, input: {
|
|
99
|
+
holderId: string;
|
|
100
|
+
expiresAt?: string | null;
|
|
101
|
+
chatId?: string;
|
|
102
|
+
}): Promise<DelegateContextGrantResult>;
|
|
83
103
|
revokeGrant(grantId: string): Promise<{
|
|
84
104
|
status: string;
|
|
85
105
|
revokedCount?: number;
|
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
-
import { ApiError } from '../types.js';
|
|
4
3
|
import { buildOperatorHeaders } from './operatorHeaders.js';
|
|
5
|
-
|
|
6
|
-
let message = defaultMessage;
|
|
7
|
-
if (responseBody) {
|
|
8
|
-
try {
|
|
9
|
-
const d = JSON.parse(responseBody);
|
|
10
|
-
message =
|
|
11
|
-
d['details'] ||
|
|
12
|
-
d['error'] ||
|
|
13
|
-
d['message'] ||
|
|
14
|
-
defaultMessage;
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
message = responseBody || defaultMessage;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
throw new ApiError(message, response.status, responseBody);
|
|
21
|
-
}
|
|
4
|
+
import { throwApiError } from '../shared/apiError.js';
|
|
22
5
|
/**
|
|
23
6
|
* ZIG-411 context grant management — list / issue / delegate / revoke.
|
|
24
7
|
*/
|
|
@@ -113,6 +96,41 @@ export class ContextGrantsClient {
|
|
|
113
96
|
truncatedAgreements: parsed.truncatedAgreements ?? 0,
|
|
114
97
|
};
|
|
115
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* ZIG-1037 — share an artifact THIS agent authored with another agent.
|
|
101
|
+
*
|
|
102
|
+
* Not a delegation: an agent holds no grant over its own output, so there is
|
|
103
|
+
* no parent to attenuate. Authorship is the authority, held by the agent's
|
|
104
|
+
* owner — which is why the receiver decides the outcome. A receiver already
|
|
105
|
+
* inside the sharer's trust boundary (same owner, same org, or an active link)
|
|
106
|
+
* is granted immediately; anyone else opens a request the owner approves,
|
|
107
|
+
* exactly like `delegateGrant`'s new-party path.
|
|
108
|
+
*/
|
|
109
|
+
async shareArtifact(artifactId, input) {
|
|
110
|
+
const res = await fetch(`${this.baseUrl}/context/artifacts/${encodeURIComponent(artifactId)}/share`, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: buildOperatorHeaders(this.operatorKey, this.agentId, {
|
|
113
|
+
'content-type': 'application/json',
|
|
114
|
+
}),
|
|
115
|
+
body: JSON.stringify(input),
|
|
116
|
+
});
|
|
117
|
+
const body = await res.text().catch(() => '');
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
throwApiError(res, body, `shareArtifact failed: ${res.status} ${res.statusText}`);
|
|
120
|
+
}
|
|
121
|
+
const parsed = JSON.parse(body);
|
|
122
|
+
if (parsed.status === 'pending_approval' && parsed.agreementId) {
|
|
123
|
+
return {
|
|
124
|
+
status: 'pending_approval',
|
|
125
|
+
agreementId: parsed.agreementId,
|
|
126
|
+
ownerId: parsed.ownerId,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (!parsed.grant?.grantId) {
|
|
130
|
+
throw new Error('Invalid response: expected { grant } or { status: "pending_approval" } from POST /context/artifacts/:id/share');
|
|
131
|
+
}
|
|
132
|
+
return { status: 'granted', grant: parsed.grant };
|
|
133
|
+
}
|
|
116
134
|
async revokeGrant(grantId) {
|
|
117
135
|
const res = await fetch(`${this.baseUrl}/context/grants/${encodeURIComponent(grantId)}`, {
|
|
118
136
|
method: 'DELETE',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
-
import {
|
|
3
|
+
import { throwApiError } from '../shared/apiError.js';
|
|
4
4
|
function getMarketplaceBaseUrl() { return `${getBackendUrl()}/marketplace`; }
|
|
5
5
|
function buildHeaders(creds) {
|
|
6
6
|
return {
|
|
@@ -15,15 +15,6 @@ function assertCreds(creds, op) {
|
|
|
15
15
|
if (!creds?.agentId)
|
|
16
16
|
throw new Error(`agentId is required for ${op}`);
|
|
17
17
|
}
|
|
18
|
-
function throwApiError(response, body, defaultMsg) {
|
|
19
|
-
let msg = defaultMsg;
|
|
20
|
-
try {
|
|
21
|
-
const d = JSON.parse(body);
|
|
22
|
-
msg = d['details'] || d['error'] || d['message'] || defaultMsg;
|
|
23
|
-
}
|
|
24
|
-
catch { /**/ }
|
|
25
|
-
throw new ApiError(msg, response.status);
|
|
26
|
-
}
|
|
27
18
|
export async function publishOffer(payload, creds) {
|
|
28
19
|
assertCreds(creds, 'marketplace offer publish');
|
|
29
20
|
const res = await fetch(`${getMarketplaceBaseUrl()}/offers/publish`, {
|
|
@@ -2,20 +2,10 @@ import 'dotenv/config';
|
|
|
2
2
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
3
|
import { buildOperatorHeaders } from './operatorHeaders.js';
|
|
4
4
|
import { GrantsClient } from './GrantsClient.js';
|
|
5
|
+
import { parseErrorMessage } from '../shared/apiError.js';
|
|
5
6
|
function randomIdempotencyKey(prefix = 'op') {
|
|
6
7
|
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
7
8
|
}
|
|
8
|
-
function parseError(text, fallback) {
|
|
9
|
-
if (!text)
|
|
10
|
-
return fallback;
|
|
11
|
-
try {
|
|
12
|
-
const parsed = JSON.parse(text);
|
|
13
|
-
return parsed['error'] || parsed['message'] || text;
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return text;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
9
|
/**
|
|
20
10
|
* ZIG-894 — the one payments client for every surface (agent-sdk, ziggs-mcp,
|
|
21
11
|
* scripts). Consolidates the former agent-sdk private ZiggsPayClient. Wallet
|
|
@@ -227,7 +217,7 @@ export class PaymentsClient {
|
|
|
227
217
|
const response = await fetch(`${this.baseUrl}${path}`, init);
|
|
228
218
|
const text = await response.text();
|
|
229
219
|
if (!response.ok) {
|
|
230
|
-
const err = new Error(
|
|
220
|
+
const err = new Error(parseErrorMessage(text, `HTTP ${response.status}`));
|
|
231
221
|
err.status = response.status;
|
|
232
222
|
err.body = text;
|
|
233
223
|
throw err;
|
package/dist/http/TaskClient.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
-
import {
|
|
3
|
+
import { throwApiError } from '../shared/apiError.js';
|
|
4
4
|
function getTaskBaseUrl() { return `${getBackendUrl()}/tasks`; }
|
|
5
5
|
function buildHeaders(creds) {
|
|
6
6
|
return {
|
|
@@ -15,26 +15,6 @@ function assertCreds(creds, op) {
|
|
|
15
15
|
if (!creds?.agentId)
|
|
16
16
|
throw new Error(`agentId is required for ${op}`);
|
|
17
17
|
}
|
|
18
|
-
function parseErrorMessage(responseBody, defaultMessage) {
|
|
19
|
-
if (!responseBody)
|
|
20
|
-
return defaultMessage;
|
|
21
|
-
try {
|
|
22
|
-
const d = JSON.parse(responseBody);
|
|
23
|
-
// NestJS ValidationPipe returns { message: string[] | string, error: 'Bad
|
|
24
|
-
// Request', statusCode }. Prefer the detailed `message` over the generic
|
|
25
|
-
// `error` so the real cause (which field failed validation) is surfaced
|
|
26
|
-
// instead of a bare "Bad Request". (ZIG-832)
|
|
27
|
-
const msg = d['message'];
|
|
28
|
-
const detailed = Array.isArray(msg) ? msg.join('; ') : (typeof msg === 'string' ? msg : undefined);
|
|
29
|
-
return d['details'] || detailed || d['error'] || defaultMessage;
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return responseBody || defaultMessage;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function throwApiError(response, responseBody, defaultMessage) {
|
|
36
|
-
throw new ApiError(parseErrorMessage(responseBody, defaultMessage), response.status, responseBody);
|
|
37
|
-
}
|
|
38
18
|
function extractTask(data) {
|
|
39
19
|
if (!data || typeof data !== 'object')
|
|
40
20
|
return null;
|
package/dist/http/grants.d.ts
CHANGED
|
@@ -11,7 +11,18 @@
|
|
|
11
11
|
* watermark are presented as `temporal` / `watermark_at` caveats.
|
|
12
12
|
*/
|
|
13
13
|
export type GrantHealth = 'active' | 'expired' | 'revoked';
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The context rail's scope kinds, as VALUES — the type below is derived from
|
|
16
|
+
* them, so widening the rail is one edit rather than a type change plus however
|
|
17
|
+
* many hand-written literal lists happen to exist. A literal subset still
|
|
18
|
+
* typechecks against the union, which is how the CLI's grant listings silently
|
|
19
|
+
* stopped showing a whole scope kind after `artifact` was added; anything that
|
|
20
|
+
* means "every context scope" should read this.
|
|
21
|
+
*/
|
|
22
|
+
export declare const CONTEXT_GRANT_SCOPE_KINDS: readonly ["chat", "agreement", "org", "artifact"];
|
|
23
|
+
/** Every scope kind across every rail: context + connection + payment. */
|
|
24
|
+
export declare const GRANT_SCOPE_KINDS: readonly ["chat", "agreement", "org", "artifact", "connection", "wallet"];
|
|
25
|
+
export type GrantScopeKind = (typeof GRANT_SCOPE_KINDS)[number];
|
|
15
26
|
export interface GrantScopeView {
|
|
16
27
|
kind: GrantScopeKind;
|
|
17
28
|
id: string;
|
package/dist/http/grants.js
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context rail's scope kinds, as VALUES — the type below is derived from
|
|
3
|
+
* them, so widening the rail is one edit rather than a type change plus however
|
|
4
|
+
* many hand-written literal lists happen to exist. A literal subset still
|
|
5
|
+
* typechecks against the union, which is how the CLI's grant listings silently
|
|
6
|
+
* stopped showing a whole scope kind after `artifact` was added; anything that
|
|
7
|
+
* means "every context scope" should read this.
|
|
8
|
+
*/
|
|
9
|
+
export const CONTEXT_GRANT_SCOPE_KINDS = [
|
|
10
|
+
'chat',
|
|
11
|
+
'agreement',
|
|
12
|
+
'org',
|
|
13
|
+
// ZIG-1037: narrowest context scope
|
|
14
|
+
'artifact',
|
|
15
|
+
];
|
|
16
|
+
/** Every scope kind across every rail: context + connection + payment. */
|
|
17
|
+
export const GRANT_SCOPE_KINDS = [
|
|
18
|
+
...CONTEXT_GRANT_SCOPE_KINDS,
|
|
19
|
+
'connection',
|
|
20
|
+
'wallet',
|
|
21
|
+
];
|
|
1
22
|
/** Value of the first caveat of `type` on a grant, or undefined. */
|
|
2
23
|
export function grantCaveat(grant, type) {
|
|
3
24
|
return grant.caveats.find((c) => c.type === type)?.value;
|
package/dist/http/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { GrantsClient } from './GrantsClient.js';
|
|
|
15
15
|
export type { ListGrantsQuery, ListGrantsResult, UnreadableRail } from './GrantsClient.js';
|
|
16
16
|
export { ContextGrantsClient } from './ContextGrantsClient.js';
|
|
17
17
|
export type { ContextGrantRecord, ContextGrantScope, ContextGrantScopeKind, ContextTemporal, IssueContextGrantInput, DelegateContextGrantInput, DelegateContextGrantResult, ReachEntry, GrantReachResult, } from './ContextGrantsClient.js';
|
|
18
|
-
export { grantCaveat } from './grants.js';
|
|
18
|
+
export { grantCaveat, CONTEXT_GRANT_SCOPE_KINDS, GRANT_SCOPE_KINDS, } from './grants.js';
|
|
19
19
|
export type { GrantView, GrantScopeKind, GrantScopeView, GrantCaveatView, GrantHealth, } from './grants.js';
|
|
20
20
|
export { PaymentsClient } from './PaymentsClient.js';
|
|
21
21
|
export type { PaymentsError, WalletBalance, WalletRef, PaymentTransactionView, TransferResult, HoldResult, ReleaseResult, PaymentGrantView, PaymentGrantEnvelope, RevokeGrantResult, PaymentApproval, WaitForApprovalResult, } from './PaymentsClient.js';
|
package/dist/http/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { ContextReadClient } from './ContextReadClient.js';
|
|
|
11
11
|
export { ContextDiscoveryClient } from './ContextDiscoveryClient.js';
|
|
12
12
|
export { GrantsClient } from './GrantsClient.js';
|
|
13
13
|
export { ContextGrantsClient } from './ContextGrantsClient.js';
|
|
14
|
-
export { grantCaveat } from './grants.js';
|
|
14
|
+
export { grantCaveat, CONTEXT_GRANT_SCOPE_KINDS, GRANT_SCOPE_KINDS, } from './grants.js';
|
|
15
15
|
export { PaymentsClient } from './PaymentsClient.js';
|
|
16
16
|
export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
|
|
17
17
|
export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, } from './OrgsClient.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -7,5 +7,6 @@ export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET,
|
|
|
7
7
|
export { getBackendUrl, getWebSocketUrl } from './utils/urlUtils.js';
|
|
8
8
|
export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
|
|
9
9
|
export { RateLimitedError, isRateLimited, parseRetryAfterMs, pollSurfaceError, } from './shared/rateLimit.js';
|
|
10
|
+
export { parseErrorMessage, throwApiError } from './shared/apiError.js';
|
|
10
11
|
export type { Creds, Task, TaskState, PlanStep, PlanStepStatus, Agreement, EngagementKind, BroadcastAudience, EntryType, ContentType, MessageMetadata, MessageHandler, ApiError, } from './types.js';
|
|
11
12
|
export type { ProposeTerms, ProposeDirectInput, ProposeBroadcastInput, ProposeAgreementData, } from './http/AgreementClient.js';
|
package/dist/index.js
CHANGED
|
@@ -7,3 +7,7 @@ export { getBackendUrl, getWebSocketUrl } from './utils/urlUtils.js';
|
|
|
7
7
|
export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
|
|
8
8
|
// ZIG-1019: retry loops need the server's own wait, not a guess.
|
|
9
9
|
export { RateLimitedError, isRateLimited, parseRetryAfterMs, pollSurfaceError, } from './shared/rateLimit.js';
|
|
10
|
+
// One reader for the one error shape the API answers in — exported so nothing has
|
|
11
|
+
// to re-implement the field precedence (it was copy-pasted into six clients, and
|
|
12
|
+
// half of them had drifted).
|
|
13
|
+
export { parseErrorMessage, throwApiError } from './shared/apiError.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the message out of a Ziggs error response.
|
|
3
|
+
*
|
|
4
|
+
* The API answers every failure in one shape — `{ error: "<what went wrong>" }`,
|
|
5
|
+
* plus `code` when the server supplied a machine-readable one — because one global
|
|
6
|
+
* exception filter builds it (backend `common/http-exception.filter.ts`, pinned in
|
|
7
|
+
* `common/error-response.spec.ts`). So `error` is read first and is normally the
|
|
8
|
+
* whole answer.
|
|
9
|
+
*
|
|
10
|
+
* `message` and `details` remain as fallbacks for responses that did NOT come from
|
|
11
|
+
* that filter: a proxy, a gateway, or a Nest-shaped body from some other service.
|
|
12
|
+
* On a Nest-shaped body `error` holds the reason phrase ("Bad Request") while
|
|
13
|
+
* `message` holds the real cause, so when both are present the more specific one
|
|
14
|
+
* wins — that ordering is why this lives in ONE place. It used to be copy-pasted
|
|
15
|
+
* into five clients, two of which had been fixed for it and three of which had not,
|
|
16
|
+
* so the same failure read differently depending on which client you called.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseErrorMessage(responseBody: string, defaultMessage: string): string;
|
|
19
|
+
/** Throw the parsed failure as an {@link ApiError}, preserving status and body. */
|
|
20
|
+
export declare function throwApiError(response: {
|
|
21
|
+
status: number;
|
|
22
|
+
}, responseBody: string, defaultMessage: string): never;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ApiError } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read the message out of a Ziggs error response.
|
|
4
|
+
*
|
|
5
|
+
* The API answers every failure in one shape — `{ error: "<what went wrong>" }`,
|
|
6
|
+
* plus `code` when the server supplied a machine-readable one — because one global
|
|
7
|
+
* exception filter builds it (backend `common/http-exception.filter.ts`, pinned in
|
|
8
|
+
* `common/error-response.spec.ts`). So `error` is read first and is normally the
|
|
9
|
+
* whole answer.
|
|
10
|
+
*
|
|
11
|
+
* `message` and `details` remain as fallbacks for responses that did NOT come from
|
|
12
|
+
* that filter: a proxy, a gateway, or a Nest-shaped body from some other service.
|
|
13
|
+
* On a Nest-shaped body `error` holds the reason phrase ("Bad Request") while
|
|
14
|
+
* `message` holds the real cause, so when both are present the more specific one
|
|
15
|
+
* wins — that ordering is why this lives in ONE place. It used to be copy-pasted
|
|
16
|
+
* into five clients, two of which had been fixed for it and three of which had not,
|
|
17
|
+
* so the same failure read differently depending on which client you called.
|
|
18
|
+
*/
|
|
19
|
+
export function parseErrorMessage(responseBody, defaultMessage) {
|
|
20
|
+
if (!responseBody)
|
|
21
|
+
return defaultMessage;
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(responseBody);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Not JSON at all — an HTML error page or a bare string is still better than
|
|
28
|
+
// nothing, so hand back what the server actually said.
|
|
29
|
+
return responseBody || defaultMessage;
|
|
30
|
+
}
|
|
31
|
+
// Valid JSON that is not an object: a quoted string IS the message; anything
|
|
32
|
+
// else (null, a number) carries no message, and echoing its raw text would put
|
|
33
|
+
// "null" in front of a caller.
|
|
34
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
35
|
+
return typeof parsed === 'string' && parsed ? parsed : defaultMessage;
|
|
36
|
+
}
|
|
37
|
+
const body = parsed;
|
|
38
|
+
const text = (value) => {
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
const joined = value.map(String).filter(Boolean).join('; ');
|
|
41
|
+
return joined || undefined;
|
|
42
|
+
}
|
|
43
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
44
|
+
};
|
|
45
|
+
const error = text(body['error']);
|
|
46
|
+
const message = text(body['message']);
|
|
47
|
+
// A Nest-shaped body carries both: prefer the specific `message` over the
|
|
48
|
+
// reason phrase sitting in `error`.
|
|
49
|
+
const preferMessage = !!message && !!body['statusCode'];
|
|
50
|
+
return (text(body['details']) ??
|
|
51
|
+
(preferMessage ? message : (error ?? message)) ??
|
|
52
|
+
defaultMessage);
|
|
53
|
+
}
|
|
54
|
+
/** Throw the parsed failure as an {@link ApiError}, preserving status and body. */
|
|
55
|
+
export function throwApiError(response, responseBody, defaultMessage) {
|
|
56
|
+
throw new ApiError(parseErrorMessage(responseBody, defaultMessage), response.status, responseBody);
|
|
57
|
+
}
|