@ziggs-ai/api-client 0.10.3 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/capabilities/agreementVerbs.d.ts +2 -2
  2. package/dist/capabilities/agreementVerbs.js +19 -19
  3. package/dist/capabilities/agreements.d.ts +1 -1
  4. package/dist/capabilities/agreements.js +5 -5
  5. package/dist/capabilities/artifacts.d.ts +4 -2
  6. package/dist/capabilities/artifacts.js +169 -34
  7. package/dist/capabilities/chat.d.ts +2 -3
  8. package/dist/capabilities/chat.js +5 -6
  9. package/dist/capabilities/connections.js +1 -1
  10. package/dist/capabilities/context.js +3 -0
  11. package/dist/capabilities/grants.d.ts +7 -6
  12. package/dist/capabilities/grants.js +9 -8
  13. package/dist/capabilities/index.d.ts +2 -2
  14. package/dist/capabilities/index.js +2 -2
  15. package/dist/capabilities/links.d.ts +1 -1
  16. package/dist/capabilities/links.js +8 -11
  17. package/dist/capabilities/marketplace.js +63 -13
  18. package/dist/capabilities/payments.d.ts +24 -8
  19. package/dist/capabilities/payments.js +28 -392
  20. package/dist/capabilities/proposeProviderId.d.ts +1 -1
  21. package/dist/capabilities/proposeProviderId.js +1 -1
  22. package/dist/http/AgreementClient.d.ts +28 -22
  23. package/dist/http/AgreementClient.js +26 -17
  24. package/dist/http/ArtifactsClient.d.ts +3 -3
  25. package/dist/http/ArtifactsClient.js +6 -8
  26. package/dist/http/ChatClient.d.ts +1 -0
  27. package/dist/http/ChatClient.js +4 -1
  28. package/dist/http/ConnectionsClient.js +12 -1
  29. package/dist/http/ContextGrantsClient.d.ts +15 -1
  30. package/dist/http/ContextGrantsClient.js +2 -0
  31. package/dist/http/ContextReadClient.d.ts +18 -8
  32. package/dist/http/ContextReadClient.js +4 -3
  33. package/dist/http/GrantsClient.d.ts +14 -0
  34. package/dist/http/GrantsClient.js +18 -2
  35. package/dist/http/InboxClient.d.ts +1 -1
  36. package/dist/http/InboxClient.js +1 -1
  37. package/dist/http/MarketplaceClient.d.ts +6 -6
  38. package/dist/http/MarketplaceClient.js +11 -11
  39. package/dist/http/TaskClient.d.ts +13 -2
  40. package/dist/http/TaskClient.js +4 -2
  41. package/dist/http/agreementFlows.d.ts +4 -4
  42. package/dist/http/agreementFlows.js +9 -10
  43. package/dist/http/grants.d.ts +28 -0
  44. package/dist/http/index.d.ts +2 -2
  45. package/dist/index.d.ts +1 -1
  46. package/dist/types.d.ts +59 -22
  47. package/dist/types.js +6 -6
  48. package/package.json +1 -1
@@ -3,4 +3,4 @@
3
3
  * tools. Stated on the schema so a fresh agent does not burn a turn learning
4
4
  * the rule from the validation error.
5
5
  */
6
- export declare const AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION = "REQUIRED on a direct proposal: name who does the work \u2014 your own id (you are offering to work) or the proposedTo id (you are commissioning the recipient). Do not omit it when proposedTo is a person/agent id. Broadcast (proposedTo everyone/org): omit for a quest (claimer works), or your own id for a standing offer (you work). A third-party id brokers and needs a matching published offer. Payer is always the non-providing side.";
6
+ export declare const AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION = "REQUIRED on a direct proposal: name who does the work \u2014 your own id (you are offering to work) or the proposedTo id (you are commissioning the recipient). Do not omit it when proposedTo is a person/agent id. Broadcast (proposedTo everyone/org): omit for a request (claimer works), or your own id for a standing offer (you work). A third-party id brokers and needs a matching published offer. Payer is always the non-providing side.";
@@ -3,4 +3,4 @@
3
3
  * tools. Stated on the schema so a fresh agent does not burn a turn learning
4
4
  * the rule from the validation error.
5
5
  */
6
- export const AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION = 'REQUIRED on a direct proposal: name who does the work — your own id (you are offering to work) or the proposedTo id (you are commissioning the recipient). Do not omit it when proposedTo is a person/agent id. Broadcast (proposedTo everyone/org): omit for a quest (claimer works), or your own id for a standing offer (you work). A third-party id brokers and needs a matching published offer. Payer is always the non-providing side.';
6
+ export const AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION = 'REQUIRED on a direct proposal: name who does the work — your own id (you are offering to work) or the proposedTo id (you are commissioning the recipient). Do not omit it when proposedTo is a person/agent id. Broadcast (proposedTo everyone/org): omit for a request (claimer works), or your own id for a standing offer (you work). A third-party id brokers and needs a matching published offer. Payer is always the non-providing side.';
@@ -1,7 +1,7 @@
1
1
  import { type Creds, type Agreement, type EngagementKind, type BroadcastAudience } from '../types.js';
2
2
  /**
3
3
  * Shared proposal terms. When `engagementKind` is omitted the server defaults to `service`.
4
- * For an open buyer-broadcast quest, set `proposedTo` to a broadcast sentinel —
4
+ * For an open buyer-broadcast request, set `proposedTo` to a broadcast sentinel —
5
5
  * `OPEN_AGREEMENT_TARGET` (`"everyone"`, fully public) or `ORG_AGREEMENT_TARGET`
6
6
  * (`"org"`, scoped to your org). Prefer `proposeBroadcast({ audience })` over
7
7
  * setting `proposedTo` by hand.
@@ -49,8 +49,9 @@ export type ProposeBroadcastInput = Omit<ProposeDirectInput, 'proposedTo'> & {
49
49
  * A trust link's agreement, as every caller sees it.
50
50
  *
51
51
  * A link is reach, not commerce: it carries no money, no escrow, no execution
52
- * state and no approvals ledger. Handing the raw document over anyway put Mongo
53
- * bookkeeping in front of an LLM, which is what forbade.
52
+ * state and no approvals ledger. Handing the raw document over anyway puts
53
+ * storage bookkeeping in front of an LLM, which is what this summary exists to
54
+ * prevent.
54
55
  *
55
56
  * Lives here rather than in `capabilities/links.ts` because this is where the
56
57
  * rule is applied; that module re-exports it so the public name is
@@ -78,7 +79,7 @@ export declare function proposeAgreement(proposalData: ProposeDirectInput, creds
78
79
  /** Propose a contract to one party (user or agent). Server defaults `engagementKind` to `service`. */
79
80
  export declare function proposeDirectTo(input: ProposeDirectInput, creds: Creds): Promise<Agreement>;
80
81
  /**
81
- * Propose an open quest. `audience` ('everyone' default | 'org') picks the
82
+ * Propose an open request. `audience` ('everyone' default | 'org') picks the
82
83
  * broadcast sentinel written to `proposedTo`. Server defaults `engagementKind`
83
84
  * to `service`.
84
85
  */
@@ -145,7 +146,7 @@ export declare function resolveMyPendingApprovalPartyId(agreement: Agreement, op
145
146
  *
146
147
  * `partyId` MUST match the authenticated actor (`actor.id === partyId`): owner
147
148
  * principal when approving as human, delegate agent id when impersonating.
148
- * Canonical for hire, service, link, and quest proposals.
149
+ * Canonical for hire, service, link, and request proposals.
149
150
  */
150
151
  export declare function approveAgreementAsParty(agreementId: string, partyId: string, status: 'approved' | 'rejected', creds: Creds): Promise<Agreement>;
151
152
  export interface CounterAgreementData {
@@ -180,21 +181,26 @@ export interface GetMyAgreementsFilters {
180
181
  export declare function getMyAgreements(filters: GetMyAgreementsFilters | undefined, creds: Creds): Promise<Agreement[]>;
181
182
  /** One agreement, shaped: a link comes back as its summary. */
182
183
  export declare function getAgreement(agreementId: string, creds: Creds): Promise<Agreement | null>;
183
- export interface CreateAgreementBody {
184
- proposedToId?: string;
185
- providerId?: string;
186
- agentId?: string;
187
- price?: number;
188
- lifecycle?: string;
189
- expiresAt?: string;
190
- maxExecutions?: number;
184
+ export interface CreateLinkBody {
185
+ /** Target agent for a direct link. Omit for an open, claimable invite. */
186
+ targetAgentId?: string;
187
+ /** Message shown to whoever is asked to approve or claim it. */
191
188
  description?: string;
192
- engagementKind?: EngagementKind;
193
- /** Link invites only: how many people may claim this one link. Default 1. */
189
+ /** Open invites only: how many people may claim this one link. Default 1. */
194
190
  maxClaims?: number;
195
- metadata?: Record<string, unknown>;
191
+ /** Which of the caller's own agents stands on their side of the link. */
192
+ asAgentId?: string;
196
193
  }
197
- export declare function createAgreement(body: CreateAgreementBody, creds: Creds): Promise<{
194
+ /**
195
+ * Create a trust link — a direct proposal to one agent, or an open invite.
196
+ *
197
+ * This posts to `POST /agreements/links`. It used to post to `POST /agreements`,
198
+ * which also created work agreements inline and skipped the gates the proposal
199
+ * and marketplace paths run. Work agreements go through `proposeAgreement`
200
+ * (`POST /agreements/proposals`) and the marketplace claim routes; this rail
201
+ * carries links and nothing else.
202
+ */
203
+ export declare function createLink(body: CreateLinkBody, creds: Creds): Promise<{
198
204
  ok: boolean;
199
205
  agreement: Agreement;
200
206
  }>;
@@ -208,13 +214,13 @@ export declare function fulfillAgreement(agreementId: string, creds: Creds): Pro
208
214
  ok: boolean;
209
215
  agreement: Agreement;
210
216
  }>;
211
- /** What an open-broadcast claim turned out to be. ⚠️ SYNC: backend AgreementOpenService. */
212
- export type ClaimedKind = 'link' | 'offer' | 'quest' | 'hand-off';
217
+ /** What an open-broadcast claim resolved to. ⚠️ Mirrors the server; it is authoritative. */
218
+ export type ClaimedKind = 'link' | 'offer' | 'request' | 'hand-off';
213
219
  /**
214
220
  * Claim an open agreement. Three shapes are claimable:
215
221
  * - open link invite (`engagementKind: link`, proposedTo everyone)
216
- * - open broadcast quest (proposedTo 'everyone', status open)
217
- * - org-scoped broadcast quest (proposedTo 'org') — the server rejects the
222
+ * - open broadcast request (proposedTo 'everyone', status open)
223
+ * - org-scoped broadcast request (proposedTo 'org') — the server rejects the
218
224
  * claim with 403 if the caller is not a member of the agreement's orgId
219
225
  *
220
226
  * POST /agreements/:id/claim — canonical; no partyId in body.
@@ -261,7 +267,7 @@ export declare class AgreementClient {
261
267
  list(filters?: ListAgreementsFilters): Promise<Agreement[]>;
262
268
  listMine(filters?: GetMyAgreementsFilters): Promise<Agreement[]>;
263
269
  get(id: string): Promise<Agreement | null>;
264
- create(data: CreateAgreementBody): Promise<{
270
+ createLink(data: CreateLinkBody): Promise<{
265
271
  ok: boolean;
266
272
  agreement: Agreement;
267
273
  }>;
@@ -28,8 +28,9 @@ function assertCreds(creds, op) {
28
28
  * A trust link's agreement, as every caller sees it.
29
29
  *
30
30
  * A link is reach, not commerce: it carries no money, no escrow, no execution
31
- * state and no approvals ledger. Handing the raw document over anyway put Mongo
32
- * bookkeeping in front of an LLM, which is what forbade.
31
+ * state and no approvals ledger. Handing the raw document over anyway puts
32
+ * storage bookkeeping in front of an LLM, which is what this summary exists to
33
+ * prevent.
33
34
  *
34
35
  * Lives here rather than in `capabilities/links.ts` because this is where the
35
36
  * rule is applied; that module re-exports it so the public name is
@@ -102,7 +103,7 @@ export async function proposeDirectTo(input, creds) {
102
103
  return proposeAgreement(input, creds);
103
104
  }
104
105
  /**
105
- * Propose an open quest. `audience` ('everyone' default | 'org') picks the
106
+ * Propose an open request. `audience` ('everyone' default | 'org') picks the
106
107
  * broadcast sentinel written to `proposedTo`. Server defaults `engagementKind`
107
108
  * to `service`.
108
109
  */
@@ -171,7 +172,7 @@ export async function respondToAgreement(agreementId, action, creds, opts = {})
171
172
  const proposedTo = agreement.parties?.proposedTo;
172
173
  if (isBroadcastTarget(proposedTo) || isBroadcastTarget(agreement.parties?.payer)) {
173
174
  // respond is approve/reject on DIRECT proposals only. An open
174
- // broadcast (quest, standing offer, link invite) has no per-recipient
175
+ // broadcast (request, standing offer, link invite) has no per-recipient
175
176
  // approval slot — a BYSTANDER can neither approve nor reject it
176
177
  //; their move is to claim it, which has its own verb.
177
178
  //
@@ -188,7 +189,7 @@ export async function respondToAgreement(agreementId, action, creds, opts = {})
188
189
  else if (!partyId) {
189
190
  throw new Error(`No pending approval entry for this operator on agreement ${agreementId}`);
190
191
  }
191
- // ZIG-1087 / ZIG-1316 — if the slot is the principal's, the server accepts
192
+ // If the slot is the principal's, the server accepts
192
193
  // only under a live approval-authority grant (else 403). Do not pre-refuse
193
194
  // here; the PUT is the source of truth.
194
195
  return approveAgreementAsParty(agreementId, partyId, action === 'approve' ? 'approved' : 'rejected', creds);
@@ -231,7 +232,7 @@ export function resolveMyPendingApprovalPartyId(agreement, opts) {
231
232
  *
232
233
  * `partyId` MUST match the authenticated actor (`actor.id === partyId`): owner
233
234
  * principal when approving as human, delegate agent id when impersonating.
234
- * Canonical for hire, service, link, and quest proposals.
235
+ * Canonical for hire, service, link, and request proposals.
235
236
  */
236
237
  export async function approveAgreementAsParty(agreementId, partyId, status, creds) {
237
238
  if (!agreementId)
@@ -405,23 +406,31 @@ export async function getAgreement(agreementId, creds) {
405
406
  const agreement = await getAgreementDocument(agreementId, creds);
406
407
  return agreement ? shapeAgreement(agreement) : null;
407
408
  }
408
- export async function createAgreement(body, creds) {
409
+ /**
410
+ * Create a trust link — a direct proposal to one agent, or an open invite.
411
+ *
412
+ * This posts to `POST /agreements/links`. It used to post to `POST /agreements`,
413
+ * which also created work agreements inline and skipped the gates the proposal
414
+ * and marketplace paths run. Work agreements go through `proposeAgreement`
415
+ * (`POST /agreements/proposals`) and the marketplace claim routes; this rail
416
+ * carries links and nothing else.
417
+ */
418
+ export async function createLink(body, creds) {
409
419
  if (!body)
410
- throw new Error('Body is required for agreement creation');
411
- assertCreds(creds, 'agreement creation');
412
- // Canonical REST create: POST /agreements.
413
- const res = await fetch(getAgreementBaseUrl(), {
420
+ throw new Error('Body is required for link creation');
421
+ assertCreds(creds, 'link creation');
422
+ const res = await fetch(`${getAgreementBaseUrl()}/links`, {
414
423
  method: 'POST',
415
424
  headers: buildHeaders(creds),
416
425
  body: JSON.stringify(body),
417
426
  });
418
427
  if (!res.ok) {
419
428
  const responseBody = await res.text().catch(() => '');
420
- throwApiError(res, responseBody, `Agreement creation failed: ${res.status} ${res.statusText}`);
429
+ throwApiError(res, responseBody, `Link creation failed: ${res.status} ${res.statusText}`);
421
430
  }
422
431
  const data = await res.json().catch(() => null);
423
432
  if (!data?.['agreement']) {
424
- throw new Error('Invalid response: expected { ok, agreement } from POST /agreements');
433
+ throw new Error('Invalid response: expected { ok, agreement } from POST /agreements/links');
425
434
  }
426
435
  const envelope = data;
427
436
  return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
@@ -471,8 +480,8 @@ export async function fulfillAgreement(agreementId, creds) {
471
480
  /**
472
481
  * Claim an open agreement. Three shapes are claimable:
473
482
  * - open link invite (`engagementKind: link`, proposedTo everyone)
474
- * - open broadcast quest (proposedTo 'everyone', status open)
475
- * - org-scoped broadcast quest (proposedTo 'org') — the server rejects the
483
+ * - open broadcast request (proposedTo 'everyone', status open)
484
+ * - org-scoped broadcast request (proposedTo 'org') — the server rejects the
476
485
  * claim with 403 if the caller is not a member of the agreement's orgId
477
486
  *
478
487
  * POST /agreements/:id/claim — canonical; no partyId in body.
@@ -492,7 +501,7 @@ export async function claimAgreement(agreementId, creds) {
492
501
  }
493
502
  // the route reports which kind of broadcast this turned out to be.
494
503
  // A caller cannot work it out from the row — post-claim no sentinel is left,
495
- // and a quest (you do the work) reads the same shape as a standing offer (you
504
+ // and a request (you do the work) reads the same shape as a standing offer (you
496
505
  // pay for it) unless you know which slot you landed in.
497
506
  const envelope = data;
498
507
  return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
@@ -682,7 +691,7 @@ export class AgreementClient {
682
691
  list(filters) { return listAgreements(filters, this.creds); }
683
692
  listMine(filters) { return getMyAgreements(filters, this.creds); }
684
693
  get(id) { return getAgreement(id, this.creds); }
685
- create(data) { return createAgreement(data, this.creds); }
694
+ createLink(data) { return createLink(data, this.creds); }
686
695
  revoke(id) { return revokeAgreement(id, this.creds); }
687
696
  fulfill(id) { return fulfillAgreement(id, this.creds); }
688
697
  claimAgreement(id) { return claimAgreement(id, this.creds); }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Inline `POST /artifacts` body cap (matches backend
3
3
  * `ARTIFACT_PUBLIC_TEXT_MAX_CHARS`). Over this → file rail or multi-part; the
4
- * server does not auto-split (ZIG-1320).
4
+ * server does not auto-split.
5
5
  */
6
6
  export declare const ARTIFACT_INLINE_TEXT_MAX_CHARS = 50000;
7
- /** ZIG-1320 — named escape hatch for agents that hit the inline cap. */
7
+ /** Named escape hatch for agents that hit the inline cap. */
8
8
  export declare const ARTIFACT_INLINE_TEXT_OVER_LIMIT_HINT: string;
9
9
  export type ArtifactVisibility = 'chat' | 'agent-private';
10
10
  export interface ListArtifactsOptions {
@@ -112,7 +112,7 @@ export declare class ArtifactsClient {
112
112
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
113
113
  * @param laneId the wake's lane (chat id, or `agrn-<agreementId>`),
114
114
  * sent as X-Ziggs-Lane so `authoredBy=me` is fenced to this engagement
115
- * instead of returning every customer's deliverables in one call.
115
+ * rather than spanning every engagement the agent has authored in.
116
116
  */
117
117
  constructor(operatorKey: string, agentId?: string, laneId?: string);
118
118
  list(q: ListArtifactsQuery, opts?: ListArtifactsOptions): Promise<ListArtifactsResult>;
@@ -5,10 +5,10 @@ import { buildOperatorHeaders } from './operatorHeaders.js';
5
5
  /**
6
6
  * Inline `POST /artifacts` body cap (matches backend
7
7
  * `ARTIFACT_PUBLIC_TEXT_MAX_CHARS`). Over this → file rail or multi-part; the
8
- * server does not auto-split (ZIG-1320).
8
+ * server does not auto-split.
9
9
  */
10
10
  export const ARTIFACT_INLINE_TEXT_MAX_CHARS = 50_000;
11
- /** ZIG-1320 — named escape hatch for agents that hit the inline cap. */
11
+ /** Named escape hatch for agents that hit the inline cap. */
12
12
  export const ARTIFACT_INLINE_TEXT_OVER_LIMIT_HINT = `text exceeds ${ARTIFACT_INLINE_TEXT_MAX_CHARS} characters. ` +
13
13
  'For larger content use ziggs_artifact_upload_url (file rail), or split into ' +
14
14
  'an index artifact plus part artifacts and list the part ids in the index. ' +
@@ -55,7 +55,7 @@ export class ArtifactsClient {
55
55
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
56
56
  * @param laneId the wake's lane (chat id, or `agrn-<agreementId>`),
57
57
  * sent as X-Ziggs-Lane so `authoredBy=me` is fenced to this engagement
58
- * instead of returning every customer's deliverables in one call.
58
+ * rather than spanning every engagement the agent has authored in.
59
59
  */
60
60
  constructor(operatorKey, agentId, laneId) {
61
61
  if (!operatorKey)
@@ -134,7 +134,7 @@ export class ArtifactsClient {
134
134
  }
135
135
  this._assertScopeXor(input);
136
136
  const text = input.text.trim();
137
- // ZIG-1320: refuse before the wire so MCP/SDK get a named escape hatch
137
+ // Refuse before the wire so MCP/SDK get a named escape hatch
138
138
  // instead of a bare class-validator string.
139
139
  if (text.length > ARTIFACT_INLINE_TEXT_MAX_CHARS) {
140
140
  throw new Error(`ArtifactsClient.writeStrict: ${ARTIFACT_INLINE_TEXT_OVER_LIMIT_HINT}`);
@@ -309,10 +309,8 @@ export class ArtifactsClient {
309
309
  if (input.chatId && input.agreementId) {
310
310
  // the refusal names the recovery, because this is the one gate
311
311
  // for every caller — the exposed tool surfaces inherit it rather than each
312
- // wording their own, and a caller that used to have chatId silently
313
- // dropped here now learns what to do instead. Wording matters: a bare
314
- // "pick one" at the last step of a finished task is what the drop was
315
- // added to avoid (dogfood).
312
+ // wording their own. Wording matters: this fires at the last step of a
313
+ // finished task, where a bare "pick one" leaves the deliverable unfiled.
316
314
  throw new Error('pass at most one of chatId or agreementId — a deliverable under an ' +
317
315
  'agreement wants agreementId alone (its parties see it); use chatId ' +
318
316
  'only for a chat-scoped note. To put it in both places, record it ' +
@@ -21,6 +21,7 @@ export interface SendChatMessageInput {
21
21
  * Recipient id. Optional: when omitted, the backend infers the
22
22
  * receiver if the chat has exactly one other member (one agent, or one
23
23
  * other human). Provide it explicitly in chats with multiple members.
24
+ * Sent on the wire as `{ id }`; the SDK still takes a string.
24
25
  */
25
26
  receiverId?: string;
26
27
  text: string;
@@ -93,7 +93,10 @@ export async function sendChatMessage(input, creds) {
93
93
  text: input.text,
94
94
  entryType,
95
95
  contentType,
96
- receiver: input.receiverId,
96
+ // Canonical wire shape is `{ id, type? }`; bare strings are still
97
+ // accepted by the backend after edge normalize, but first-party
98
+ // clients send the object.
99
+ receiver: input.receiverId ? { id: input.receiverId } : undefined,
97
100
  underAgreementId: input.underAgreementId,
98
101
  }),
99
102
  });
@@ -93,10 +93,21 @@ export class ConnectionsClient {
93
93
  async listForHolder() {
94
94
  const grantsClient = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl);
95
95
  // All pages of the agent's live connection grants (not just the first page).
96
- const items = await grantsClient.listAllGrants({
96
+ const { items, unreadableRails } = await grantsClient.listAllGrantsWithRails({
97
97
  scopeKind: 'connection',
98
98
  health: 'active',
99
99
  });
100
+ // An unreadable rail is not an empty one. Returning [] here made
101
+ // every caller say "no connection grant" when the truth was "this key may not
102
+ // look" — the needs gate refused work the agent could do, and the MCP tool
103
+ // told the owner to issue a grant that already existed. Throwing puts the
104
+ // missing scope in the message, where the person who can fix it will read it.
105
+ const railBlocked = unreadableRails.find((r) => r.rail === 'connection');
106
+ if (railBlocked && items.length === 0) {
107
+ throw new Error(`Cannot read this agent's connection grants: the operator key is missing the ` +
108
+ `"${railBlocked.requiredScope}" scope, so the grant list came back empty whether or not ` +
109
+ 'a grant exists. Re-mint the key with that scope (the "launcher-mcp-broker" preset includes it).');
110
+ }
100
111
  const byConnection = new Map();
101
112
  for (const g of items) {
102
113
  const connectionId = g.scope.id;
@@ -1,4 +1,4 @@
1
- import type { GrantView } from './grants.js';
1
+ import type { GrantAccessKind, GrantHolderKind, GrantView } from './grants.js';
2
2
  /**
3
3
  * added `artifact` — the narrowest context scope: one specific artifact,
4
4
  * shared without sharing any chat or agreement it sits in. Still the context
@@ -21,12 +21,26 @@ export interface ContextGrantScope {
21
21
  export type ContextGrantRecord = GrantView;
22
22
  export interface IssueContextGrantInput {
23
23
  holderId: string;
24
+ /**
25
+ * What kind of principal the holder is: a person, an organization, or an
26
+ * agent. Always stated, because an id says nothing about what it names — and
27
+ * an organization holder is how one grant covers a whole team.
28
+ */
29
+ holderKind: GrantHolderKind;
30
+ /**
31
+ * What the holder may do: `read` to see the scope, `write` to take part in
32
+ * it, `admit` to bring others in as well. Defaults to `write`.
33
+ */
34
+ kind?: GrantAccessKind;
24
35
  scope: ContextGrantScope;
25
36
  temporal?: ContextTemporal;
26
37
  expiresAt?: string | null;
27
38
  }
28
39
  export interface DelegateContextGrantInput {
29
40
  holderId: string;
41
+ holderKind: GrantHolderKind;
42
+ /** Never stronger than the grant it comes from; defaults to the same. */
43
+ kind?: GrantAccessKind;
30
44
  scope: ContextGrantScope;
31
45
  temporal: ContextTemporal;
32
46
  expiresAt?: string | null;
@@ -28,6 +28,8 @@ export class ContextGrantsClient {
28
28
  }),
29
29
  body: JSON.stringify({
30
30
  holderId: input.holderId,
31
+ holderKind: input.holderKind,
32
+ kind: input.kind,
31
33
  scope: input.scope,
32
34
  temporal: input.temporal,
33
35
  expiresAt: input.expiresAt,
@@ -43,17 +43,26 @@ export interface ContextReadEnvelope<T = unknown> {
43
43
  latestSequence?: string | null;
44
44
  }
45
45
  /** Aggregated chat snapshot from `GET /context/snapshot` (follow-up). */
46
+ /**
47
+ * One participant on the snapshot roster. `kind` is the declared holder kind the
48
+ * server stamped on the row, so a reader takes what a participant IS from the
49
+ * row rather than from which array it arrived in. Optional so a backend that
50
+ * predates it still parses.
51
+ */
52
+ export interface ContextSnapshotParticipant {
53
+ role: string;
54
+ kind?: string;
55
+ isYou?: boolean;
56
+ [key: string]: unknown;
57
+ }
46
58
  export interface ContextSnapshotResult {
47
59
  history: unknown[];
48
60
  agreements: unknown[];
49
- agents: Array<{
61
+ agents: Array<ContextSnapshotParticipant & {
50
62
  agentId: string;
51
- role: string;
52
- isYou: boolean;
53
63
  }>;
54
- users: Array<{
64
+ users: Array<ContextSnapshotParticipant & {
55
65
  userId: string;
56
- role: string;
57
66
  }>;
58
67
  latestSequence?: string | null;
59
68
  chatMissing?: boolean;
@@ -70,9 +79,10 @@ export declare class ContextReadClient {
70
79
  /**
71
80
  * @param operatorKey Agent-scoped or fleet operator key.
72
81
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
73
- * @param laneId the wake's lane, sent as X-Ziggs-Lane. This is the
74
- * path the dogfood leak ran through: `via=artifact:<other customer's spec>`
75
- * was authorised purely because the same agent had authored it.
82
+ * @param laneId the wake's lane, sent as X-Ziggs-Lane. The server fences a
83
+ * read to the lane's parties; without it, reach falls back to a wider
84
+ * test and a read can be authorised on a weaker basis than the caller
85
+ * intended. Send it on every read made while acting on a wake.
76
86
  */
77
87
  constructor(operatorKey: string, agentId?: string, baseUrl?: string, laneId?: string);
78
88
  read<T = unknown>(type: ContextReadType, query: ContextReadQuery): Promise<ContextReadEnvelope<T>>;
@@ -60,9 +60,10 @@ export class ContextReadClient {
60
60
  /**
61
61
  * @param operatorKey Agent-scoped or fleet operator key.
62
62
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
63
- * @param laneId the wake's lane, sent as X-Ziggs-Lane. This is the
64
- * path the dogfood leak ran through: `via=artifact:<other customer's spec>`
65
- * was authorised purely because the same agent had authored it.
63
+ * @param laneId the wake's lane, sent as X-Ziggs-Lane. The server fences a
64
+ * read to the lane's parties; without it, reach falls back to a wider
65
+ * test and a read can be authorised on a weaker basis than the caller
66
+ * intended. Send it on every read made while acting on a wake.
66
67
  */
67
68
  constructor(operatorKey, agentId, baseUrl, laneId) {
68
69
  if (!operatorKey)
@@ -66,4 +66,18 @@ export declare class GrantsClient {
66
66
  * truncated. `maxPages` bounds the loop as a runaway guard.
67
67
  */
68
68
  listAllGrants(query?: Omit<ListGrantsQuery, 'cursor'>, maxPages?: number): Promise<GrantView[]>;
69
+ /**
70
+ * The same sweep, keeping the part `listAllGrants` throws away: which rails the
71
+ * caller was not allowed to read.
72
+ *
73
+ * The distinction is not cosmetic. An empty page from an unreadable rail looks
74
+ * exactly like "you hold no grants", and a caller that cannot tell them apart
75
+ * reports the wrong one: a brokered connector agent with a live grant refused
76
+ * its task and told the buyer to ask for the grant they had just issued, because
77
+ * its key lacked `connections:read` and this method answered "none".
78
+ */
79
+ listAllGrantsWithRails(query?: Omit<ListGrantsQuery, 'cursor'>, maxPages?: number): Promise<{
80
+ items: GrantView[];
81
+ unreadableRails: UnreadableRail[];
82
+ }>;
69
83
  }
@@ -65,15 +65,31 @@ export class GrantsClient {
65
65
  * truncated. `maxPages` bounds the loop as a runaway guard.
66
66
  */
67
67
  async listAllGrants(query = {}, maxPages = 50) {
68
+ return (await this.listAllGrantsWithRails(query, maxPages)).items;
69
+ }
70
+ /**
71
+ * The same sweep, keeping the part `listAllGrants` throws away: which rails the
72
+ * caller was not allowed to read.
73
+ *
74
+ * The distinction is not cosmetic. An empty page from an unreadable rail looks
75
+ * exactly like "you hold no grants", and a caller that cannot tell them apart
76
+ * reports the wrong one: a brokered connector agent with a live grant refused
77
+ * its task and told the buyer to ask for the grant they had just issued, because
78
+ * its key lacked `connections:read` and this method answered "none".
79
+ */
80
+ async listAllGrantsWithRails(query = {}, maxPages = 50) {
68
81
  const all = [];
82
+ const rails = new Map();
69
83
  let cursor;
70
84
  for (let page = 0; page < maxPages; page++) {
71
85
  const res = await this.listGrants({ ...query, cursor });
72
86
  all.push(...res.items);
87
+ for (const rail of res.unreadableRails ?? [])
88
+ rails.set(rail.rail, rail);
73
89
  if (!res.nextCursor)
74
- return all;
90
+ break;
75
91
  cursor = res.nextCursor;
76
92
  }
77
- return all;
93
+ return { items: all, unreadableRails: [...rails.values()] };
78
94
  }
79
95
  }
@@ -18,7 +18,7 @@ export declare class InboxClient {
18
18
  getInbox(opts?: InboxReadOptions): Promise<InboxEnvelope>;
19
19
  /**
20
20
  * Advance this agent's watermark — pass the envelope's `ackTo` plus every
21
- * `resourceId` handled in `(priorAck, upTo]` (ZIG-1305). Monotonic
21
+ * `resourceId` handled in `(priorAck, upTo]`. Monotonic
22
22
  * server-side: an older value is a no-op, so a replayed ack can never
23
23
  * redeliver handled work. An ack that would bury unlisted deliveries is
24
24
  * refused.
@@ -48,7 +48,7 @@ export class InboxClient {
48
48
  }
49
49
  /**
50
50
  * Advance this agent's watermark — pass the envelope's `ackTo` plus every
51
- * `resourceId` handled in `(priorAck, upTo]` (ZIG-1305). Monotonic
51
+ * `resourceId` handled in `(priorAck, upTo]`. Monotonic
52
52
  * server-side: an older value is a no-op, so a replayed ack can never
53
53
  * redeliver handled work. An ack that would bury unlisted deliveries is
54
54
  * refused.
@@ -24,7 +24,7 @@ export interface PullOffersOptions {
24
24
  }
25
25
  export declare function pullOffers(options: PullOffersOptions | undefined, creds: Creds): Promise<Agreement[]>;
26
26
  export declare function claimOffer(agreementId: string, creds: Creds): Promise<Agreement>;
27
- export interface PublishQuestPayload {
27
+ export interface PublishRequestPayload {
28
28
  /** See PublishOfferPayload.billing. */
29
29
  billing?: 'total' | 'per_task';
30
30
  description: string;
@@ -38,12 +38,12 @@ export interface PublishQuestPayload {
38
38
  /** Broadcast audience: 'everyone' (default, fully public) or 'org' (members of your active org only). */
39
39
  audience?: BroadcastAudience;
40
40
  }
41
- export declare function publishQuest(payload: PublishQuestPayload, creds: Creds): Promise<Agreement>;
42
- export interface PullQuestsOptions {
41
+ export declare function publishRequest(payload: PublishRequestPayload, creds: Creds): Promise<Agreement>;
42
+ export interface PullRequestsOptions {
43
43
  limit?: number;
44
44
  since?: string;
45
45
  }
46
- export declare function pullQuests(options: PullQuestsOptions | undefined, creds: Creds): Promise<Agreement[]>;
46
+ export declare function pullRequests(options: PullRequestsOptions | undefined, creds: Creds): Promise<Agreement[]>;
47
47
  export declare class MarketplaceClient {
48
48
  private creds;
49
49
  /**
@@ -54,6 +54,6 @@ export declare class MarketplaceClient {
54
54
  publishOffer(payload: PublishOfferPayload): Promise<Agreement>;
55
55
  pullOffers(options?: PullOffersOptions): Promise<Agreement[]>;
56
56
  claimOffer(agreementId: string): Promise<Agreement>;
57
- publishQuest(payload: PublishQuestPayload): Promise<Agreement>;
58
- pullQuests(options?: PullQuestsOptions): Promise<Agreement[]>;
57
+ publishRequest(payload: PublishRequestPayload): Promise<Agreement>;
58
+ pullRequests(options?: PullRequestsOptions): Promise<Agreement[]>;
59
59
  }
@@ -70,37 +70,37 @@ export async function claimOffer(agreementId, creds) {
70
70
  throw new Error(data?.['error'] || 'Claim failed');
71
71
  return shapeAgreement(data['offer']);
72
72
  }
73
- export async function publishQuest(payload, creds) {
74
- assertCreds(creds, 'quest publish');
75
- const res = await fetch(`${getMarketplaceBaseUrl()}/quests/publish`, {
73
+ export async function publishRequest(payload, creds) {
74
+ assertCreds(creds, 'request publish');
75
+ const res = await fetch(`${getMarketplaceBaseUrl()}/requests/publish`, {
76
76
  method: 'POST',
77
77
  headers: buildHeaders(creds),
78
78
  body: JSON.stringify(payload || {}),
79
79
  });
80
80
  if (!res.ok) {
81
81
  const body = await res.text().catch(() => '');
82
- throwApiError(res, body, `Quest publish failed: ${res.status}`);
82
+ throwApiError(res, body, `Request publish failed: ${res.status}`);
83
83
  }
84
84
  const data = await res.json().catch(() => null);
85
85
  if (!data?.['agreement'])
86
- throw new Error('Quest publish returned no agreement');
86
+ throw new Error('Request publish returned no agreement');
87
87
  return shapeAgreement(data['agreement']);
88
88
  }
89
- export async function pullQuests(options, creds) {
90
- assertCreds(creds, 'quest pull');
89
+ export async function pullRequests(options, creds) {
90
+ assertCreds(creds, 'request pull');
91
91
  const params = new URLSearchParams();
92
92
  if (options?.limit != null)
93
93
  params.set('limit', String(options.limit));
94
94
  if (options?.since)
95
95
  params.set('since', options.since);
96
96
  const qs = params.toString();
97
- const res = await fetch(`${getMarketplaceBaseUrl()}/quests${qs ? `?${qs}` : ''}`, {
97
+ const res = await fetch(`${getMarketplaceBaseUrl()}/requests${qs ? `?${qs}` : ''}`, {
98
98
  method: 'GET',
99
99
  headers: buildHeaders(creds),
100
100
  });
101
101
  if (!res.ok) {
102
102
  const body = await res.text().catch(() => '');
103
- throwApiError(res, body, `Quest pull failed: ${res.status}`);
103
+ throwApiError(res, body, `Request pull failed: ${res.status}`);
104
104
  }
105
105
  const data = await res.json().catch(() => null);
106
106
  return data?.['agreements'] ?? [];
@@ -121,6 +121,6 @@ export class MarketplaceClient {
121
121
  publishOffer(payload) { return publishOffer(payload, this.creds); }
122
122
  pullOffers(options) { return pullOffers(options, this.creds); }
123
123
  claimOffer(agreementId) { return claimOffer(agreementId, this.creds); }
124
- publishQuest(payload) { return publishQuest(payload, this.creds); }
125
- pullQuests(options) { return pullQuests(options, this.creds); }
124
+ publishRequest(payload) { return publishRequest(payload, this.creds); }
125
+ pullRequests(options) { return pullRequests(options, this.creds); }
126
126
  }