@ziggs-ai/api-client 0.10.4 → 0.12.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 (51) hide show
  1. package/dist/capabilities/agreementVerbs.d.ts +9 -2
  2. package/dist/capabilities/agreementVerbs.js +61 -21
  3. package/dist/capabilities/agreements.d.ts +1 -1
  4. package/dist/capabilities/agreements.js +18 -6
  5. package/dist/capabilities/artifacts.d.ts +4 -2
  6. package/dist/capabilities/artifacts.js +168 -33
  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 +4 -4
  14. package/dist/capabilities/index.js +4 -4
  15. package/dist/capabilities/links.d.ts +17 -6
  16. package/dist/capabilities/links.js +71 -86
  17. package/dist/capabilities/marketplace.js +23 -17
  18. package/dist/capabilities/nextCall.d.ts +36 -5
  19. package/dist/capabilities/nextCall.js +53 -7
  20. package/dist/capabilities/payments.d.ts +24 -8
  21. package/dist/capabilities/payments.js +28 -392
  22. package/dist/capabilities/proposeProviderId.d.ts +1 -1
  23. package/dist/capabilities/proposeProviderId.js +1 -1
  24. package/dist/http/AgreementClient.d.ts +63 -27
  25. package/dist/http/AgreementClient.js +51 -39
  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 +14 -5
  32. package/dist/http/GrantsClient.d.ts +14 -0
  33. package/dist/http/GrantsClient.js +18 -2
  34. package/dist/http/InboxClient.js +4 -0
  35. package/dist/http/MarketplaceClient.d.ts +6 -8
  36. package/dist/http/MarketplaceClient.js +11 -30
  37. package/dist/http/TaskClient.d.ts +5 -0
  38. package/dist/http/TaskClient.js +4 -7
  39. package/dist/http/agreementFlows.d.ts +6 -7
  40. package/dist/http/agreementFlows.js +14 -20
  41. package/dist/http/grants.d.ts +28 -0
  42. package/dist/http/index.d.ts +2 -2
  43. package/dist/index.d.ts +3 -3
  44. package/dist/index.js +1 -1
  45. package/dist/instanceIdentity.d.ts +4 -0
  46. package/dist/instanceIdentity.js +44 -0
  47. package/dist/relay/provisionRelayWorkers.d.ts +2 -2
  48. package/dist/relay/provisionRelayWorkers.js +5 -5
  49. package/dist/types.d.ts +80 -31
  50. package/dist/types.js +18 -0
  51. package/package.json +1 -1
@@ -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.
@@ -79,7 +79,7 @@ export declare function proposeAgreement(proposalData: ProposeDirectInput, creds
79
79
  /** Propose a contract to one party (user or agent). Server defaults `engagementKind` to `service`. */
80
80
  export declare function proposeDirectTo(input: ProposeDirectInput, creds: Creds): Promise<Agreement>;
81
81
  /**
82
- * Propose an open quest. `audience` ('everyone' default | 'org') picks the
82
+ * Propose an open request. `audience` ('everyone' default | 'org') picks the
83
83
  * broadcast sentinel written to `proposedTo`. Server defaults `engagementKind`
84
84
  * to `service`.
85
85
  */
@@ -124,8 +124,12 @@ export declare function respondToAgreement(agreementId: string, action: 'approve
124
124
  export interface PendingApprovalFacts {
125
125
  /** Party ids with a pending entry on the approvals ledger. */
126
126
  pendingPartyIds: readonly string[];
127
- /** The direct responder slot, when the proposal names one. */
128
- proposedTo?: string | null;
127
+ /**
128
+ * The direct responder slot, when the proposal names one. A side names up to
129
+ * two ids — the accountable principal and the delegate acting for it — and
130
+ * either may be the one holding the decision.
131
+ */
132
+ proposedToIds?: readonly string[];
129
133
  }
130
134
  /**
131
135
  * Which of `candidateIds` holds the pending approval slot, first match wins —
@@ -146,7 +150,7 @@ export declare function resolveMyPendingApprovalPartyId(agreement: Agreement, op
146
150
  *
147
151
  * `partyId` MUST match the authenticated actor (`actor.id === partyId`): owner
148
152
  * principal when approving as human, delegate agent id when impersonating.
149
- * Canonical for hire, service, link, and quest proposals.
153
+ * Canonical for hire, service, link, and request proposals.
150
154
  */
151
155
  export declare function approveAgreementAsParty(agreementId: string, partyId: string, status: 'approved' | 'rejected', creds: Creds): Promise<Agreement>;
152
156
  export interface CounterAgreementData {
@@ -181,24 +185,42 @@ export interface GetMyAgreementsFilters {
181
185
  export declare function getMyAgreements(filters: GetMyAgreementsFilters | undefined, creds: Creds): Promise<Agreement[]>;
182
186
  /** One agreement, shaped: a link comes back as its summary. */
183
187
  export declare function getAgreement(agreementId: string, creds: Creds): Promise<Agreement | null>;
184
- export interface CreateAgreementBody {
185
- proposedToId?: string;
186
- providerId?: string;
187
- agentId?: string;
188
- price?: number;
189
- lifecycle?: string;
190
- expiresAt?: string;
191
- maxExecutions?: number;
188
+ export interface CreateLinkBody {
189
+ /**
190
+ * Who to connect with: an email address, or the id of an agent that answers
191
+ * to them. Either way the link is with the PERSON — an id is only a way to
192
+ * find its owner, and an agent that answers to an org is refused. Omit for an
193
+ * open invite you share yourself.
194
+ */
195
+ to?: string;
196
+ /** Message shown to whoever is asked to approve or claim it. */
192
197
  description?: string;
193
- engagementKind?: EngagementKind;
194
- /** Link invites only: how many people may claim this one link. Default 1. */
198
+ /** Open invites only: how many people may claim this one link. Default 1. */
195
199
  maxClaims?: number;
196
- metadata?: Record<string, unknown>;
197
200
  }
198
- export declare function createAgreement(body: CreateAgreementBody, creds: Creds): Promise<{
201
+ /**
202
+ * The route's one answer, whatever was in `to`.
203
+ *
204
+ * Deliberately carries no agreement. Returning the row for an id target and a
205
+ * bare "sent" for an address made one of the two an existence oracle over the
206
+ * user table, so both say the same thing now: it went. `shareUrl` is null
207
+ * exactly when `to` was named, which the caller already knows.
208
+ */
209
+ export interface CreateLinkResult {
199
210
  ok: boolean;
200
- agreement: Agreement;
201
- }>;
211
+ status: 'sent';
212
+ shareUrl: string | null;
213
+ }
214
+ /**
215
+ * Connect with someone — by email, by agent id, or with a shareable invite.
216
+ *
217
+ * This posts to `POST /agreements/links`. It used to post to `POST /agreements`,
218
+ * which also created work agreements inline and skipped the gates the proposal
219
+ * and marketplace paths run. Work agreements go through `proposeAgreement`
220
+ * (`POST /agreements/proposals`) and the marketplace claim routes; this rail
221
+ * carries links and nothing else.
222
+ */
223
+ export declare function createLink(body: CreateLinkBody, creds: Creds): Promise<CreateLinkResult>;
202
224
  export declare function revokeAgreement(agreementId: string, creds: Creds): Promise<{
203
225
  ok: boolean;
204
226
  agreement: Agreement;
@@ -210,17 +232,34 @@ export declare function fulfillAgreement(agreementId: string, creds: Creds): Pro
210
232
  agreement: Agreement;
211
233
  }>;
212
234
  /** What an open-broadcast claim resolved to. ⚠️ Mirrors the server; it is authoritative. */
213
- export type ClaimedKind = 'link' | 'offer' | 'quest' | 'hand-off';
235
+ export type ClaimedKind = 'link' | 'offer' | 'request' | 'hand-off';
214
236
  /**
215
237
  * Claim an open agreement. Three shapes are claimable:
216
238
  * - open link invite (`engagementKind: link`, proposedTo everyone)
217
- * - open broadcast quest (proposedTo 'everyone', status open)
218
- * - org-scoped broadcast quest (proposedTo 'org') — the server rejects the
239
+ * - open broadcast request (proposedTo 'everyone', status open)
240
+ * - org-scoped broadcast request (proposedTo 'org') — the server rejects the
219
241
  * claim with 403 if the caller is not a member of the agreement's orgId
220
242
  *
221
243
  * POST /agreements/:id/claim — canonical; no partyId in body.
222
244
  */
223
- export declare function claimAgreement(agreementId: string, creds: Creds): Promise<{
245
+ /**
246
+ * What a claimer can say about WHY it is claiming.
247
+ *
248
+ * A claim binds a party to terms somebody else posted, so the only thing there
249
+ * is to declare is the authority behind it: the job the claiming agent is
250
+ * already doing. Nothing here changes the row — the lineage of a posted
251
+ * agreement belongs to whoever posted it.
252
+ */
253
+ export interface ClaimOptions {
254
+ /**
255
+ * The active agreement whose work this claim is part of. An agent acting
256
+ * inside a job its human approved does not need a second consent for the
257
+ * engagements that job requires, but it has to NAME the job — the server
258
+ * verifies the claim against the acting agent and never guesses one.
259
+ */
260
+ mandateAgreementId?: string;
261
+ }
262
+ export declare function claimAgreement(agreementId: string, creds: Creds, opts?: ClaimOptions): Promise<{
224
263
  ok: boolean;
225
264
  agreement: Agreement;
226
265
  kind?: ClaimedKind;
@@ -262,10 +301,7 @@ export declare class AgreementClient {
262
301
  list(filters?: ListAgreementsFilters): Promise<Agreement[]>;
263
302
  listMine(filters?: GetMyAgreementsFilters): Promise<Agreement[]>;
264
303
  get(id: string): Promise<Agreement | null>;
265
- create(data: CreateAgreementBody): Promise<{
266
- ok: boolean;
267
- agreement: Agreement;
268
- }>;
304
+ createLink(data: CreateLinkBody): Promise<CreateLinkResult>;
269
305
  revoke(id: string): Promise<{
270
306
  ok: boolean;
271
307
  agreement: Agreement;
@@ -1,6 +1,6 @@
1
1
  import { runtimeLog } from '../shared/runtimeLog.js';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
- import { OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, isBroadcastTarget } from '../types.js';
3
+ import { OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, isBroadcastTarget, partySideIds } from '../types.js';
4
4
  import { throwApiError } from '../shared/apiError.js';
5
5
  // Lazy: read at call time so a `configureApiClient` call that lands after this
6
6
  // module is imported still takes effect. Baking it at module-load time would
@@ -37,6 +37,10 @@ function assertCreds(creds, op) {
37
37
  * unchanged.
38
38
  */
39
39
  export function linkSummary(a) {
40
+ const side = (s) => ({
41
+ principal: s?.principal ?? null,
42
+ actor: s?.actor ?? null,
43
+ });
40
44
  return {
41
45
  agreementId: a.agreementId,
42
46
  // Kept deliberately: callers branch on this, and a summary that hides what
@@ -44,11 +48,11 @@ export function linkSummary(a) {
44
48
  engagementKind: a.engagementKind,
45
49
  status: a.status,
46
50
  proposalStatus: a.proposalStatus,
51
+ // No `payer`: a link carries no money, so the paying side is never occupied.
47
52
  parties: {
48
- creatorAgent: a.parties?.creatorAgent ?? null,
49
- providerAgent: a.parties?.providerAgent ?? null,
50
- creator: a.parties?.creator ?? null,
51
- proposedTo: a.parties?.proposedTo ?? null,
53
+ creator: side(a.parties?.creator),
54
+ provider: side(a.parties?.provider),
55
+ proposedTo: side(a.parties?.proposedTo),
52
56
  },
53
57
  ...(a.description ? { description: a.description } : {}),
54
58
  // Seat bookkeeping on an open invite — link state, not commerce.
@@ -103,7 +107,7 @@ export async function proposeDirectTo(input, creds) {
103
107
  return proposeAgreement(input, creds);
104
108
  }
105
109
  /**
106
- * Propose an open quest. `audience` ('everyone' default | 'org') picks the
110
+ * Propose an open request. `audience` ('everyone' default | 'org') picks the
107
111
  * broadcast sentinel written to `proposedTo`. Server defaults `engagementKind`
108
112
  * to `service`.
109
113
  */
@@ -169,10 +173,10 @@ export async function respondToAgreement(agreementId, action, creds, opts = {})
169
173
  ownerUserId: opts.ownerUserId,
170
174
  agentId: creds.agentId,
171
175
  });
172
- const proposedTo = agreement.parties?.proposedTo;
173
- if (isBroadcastTarget(proposedTo) || isBroadcastTarget(agreement.parties?.payer)) {
176
+ const proposedTo = agreement.parties?.proposedTo?.principal;
177
+ if (isBroadcastTarget(proposedTo) || isBroadcastTarget(agreement.parties?.payer?.principal)) {
174
178
  // respond is approve/reject on DIRECT proposals only. An open
175
- // broadcast (quest, standing offer, link invite) has no per-recipient
179
+ // broadcast (request, standing offer, link invite) has no per-recipient
176
180
  // approval slot — a BYSTANDER can neither approve nor reject it
177
181
  //; their move is to claim it, which has its own verb.
178
182
  //
@@ -206,11 +210,12 @@ export function resolvePendingApprovalPartyId(facts, candidateIds) {
206
210
  }
207
211
  // A row with no pending ledger entries is a legacy one (approvals[] predates
208
212
  //), and there the named responder slot IS the decision.
209
- const proposedTo = facts.proposedTo ?? null;
210
- if (proposedTo &&
211
- actorIds.includes(proposedTo) &&
212
- facts.pendingPartyIds.length === 0) {
213
- return proposedTo;
213
+ if (facts.pendingPartyIds.length === 0) {
214
+ const responderIds = facts.proposedToIds ?? [];
215
+ for (const id of actorIds) {
216
+ if (responderIds.includes(id))
217
+ return id;
218
+ }
214
219
  }
215
220
  return null;
216
221
  }
@@ -223,7 +228,7 @@ export function resolveMyPendingApprovalPartyId(agreement, opts) {
223
228
  pendingPartyIds: (agreement.approvals ?? [])
224
229
  .filter((a) => a.status === 'pending' || a.status === 'PENDING')
225
230
  .map((a) => a.partyId),
226
- proposedTo: agreement.parties?.proposedTo ?? null,
231
+ proposedToIds: partySideIds(agreement.parties?.proposedTo),
227
232
  }, [opts.agentId, opts.ownerUserId]);
228
233
  }
229
234
  /**
@@ -232,7 +237,7 @@ export function resolveMyPendingApprovalPartyId(agreement, opts) {
232
237
  *
233
238
  * `partyId` MUST match the authenticated actor (`actor.id === partyId`): owner
234
239
  * principal when approving as human, delegate agent id when impersonating.
235
- * Canonical for hire, service, link, and quest proposals.
240
+ * Canonical for hire, service, link, and request proposals.
236
241
  */
237
242
  export async function approveAgreementAsParty(agreementId, partyId, status, creds) {
238
243
  if (!agreementId)
@@ -406,26 +411,38 @@ export async function getAgreement(agreementId, creds) {
406
411
  const agreement = await getAgreementDocument(agreementId, creds);
407
412
  return agreement ? shapeAgreement(agreement) : null;
408
413
  }
409
- export async function createAgreement(body, creds) {
414
+ /**
415
+ * Connect with someone — by email, by agent id, or with a shareable invite.
416
+ *
417
+ * This posts to `POST /agreements/links`. It used to post to `POST /agreements`,
418
+ * which also created work agreements inline and skipped the gates the proposal
419
+ * and marketplace paths run. Work agreements go through `proposeAgreement`
420
+ * (`POST /agreements/proposals`) and the marketplace claim routes; this rail
421
+ * carries links and nothing else.
422
+ */
423
+ export async function createLink(body, creds) {
410
424
  if (!body)
411
- throw new Error('Body is required for agreement creation');
412
- assertCreds(creds, 'agreement creation');
413
- // Canonical REST create: POST /agreements.
414
- const res = await fetch(getAgreementBaseUrl(), {
425
+ throw new Error('Body is required for link creation');
426
+ assertCreds(creds, 'link creation');
427
+ const res = await fetch(`${getAgreementBaseUrl()}/links`, {
415
428
  method: 'POST',
416
429
  headers: buildHeaders(creds),
417
430
  body: JSON.stringify(body),
418
431
  });
419
432
  if (!res.ok) {
420
433
  const responseBody = await res.text().catch(() => '');
421
- throwApiError(res, responseBody, `Agreement creation failed: ${res.status} ${res.statusText}`);
434
+ throwApiError(res, responseBody, `Link creation failed: ${res.status} ${res.statusText}`);
422
435
  }
423
436
  const data = await res.json().catch(() => null);
424
- if (!data?.['agreement']) {
425
- throw new Error('Invalid response: expected { ok, agreement } from POST /agreements');
437
+ if (data?.['status'] !== 'sent') {
438
+ throw new Error('Invalid response: expected { ok, status: "sent", shareUrl } from POST /agreements/links');
426
439
  }
427
- const envelope = data;
428
- return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
440
+ const shareUrl = data['shareUrl'];
441
+ return {
442
+ ok: data['ok'] === true,
443
+ status: 'sent',
444
+ shareUrl: typeof shareUrl === 'string' ? shareUrl : null,
445
+ };
429
446
  }
430
447
  export async function revokeAgreement(agreementId, creds) {
431
448
  if (!agreementId)
@@ -469,20 +486,15 @@ export async function fulfillAgreement(agreementId, creds) {
469
486
  const envelope = data;
470
487
  return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
471
488
  }
472
- /**
473
- * Claim an open agreement. Three shapes are claimable:
474
- * - open link invite (`engagementKind: link`, proposedTo everyone)
475
- * - open broadcast quest (proposedTo 'everyone', status open)
476
- * - org-scoped broadcast quest (proposedTo 'org') — the server rejects the
477
- * claim with 403 if the caller is not a member of the agreement's orgId
478
- *
479
- * POST /agreements/:id/claim — canonical; no partyId in body.
480
- */
481
- export async function claimAgreement(agreementId, creds) {
489
+ export async function claimAgreement(agreementId, creds, opts = {}) {
482
490
  if (!agreementId)
483
491
  throw new Error('agreementId is required to claim an open agreement');
484
492
  assertCreds(creds, 'open agreement claim');
485
- const res = await fetch(`${getAgreementBaseUrl()}/${encodeURIComponent(agreementId)}/claim`, { method: 'POST', headers: buildHeaders(creds) });
493
+ const res = await fetch(`${getAgreementBaseUrl()}/${encodeURIComponent(agreementId)}/claim`, {
494
+ method: 'POST',
495
+ headers: buildHeaders(creds),
496
+ body: JSON.stringify(opts.mandateAgreementId ? { mandateAgreementId: opts.mandateAgreementId } : {}),
497
+ });
486
498
  if (!res.ok) {
487
499
  const body = await res.text().catch(() => '');
488
500
  throwApiError(res, body, `Open agreement claim failed: ${res.status} ${res.statusText}`);
@@ -493,7 +505,7 @@ export async function claimAgreement(agreementId, creds) {
493
505
  }
494
506
  // the route reports which kind of broadcast this turned out to be.
495
507
  // A caller cannot work it out from the row — post-claim no sentinel is left,
496
- // and a quest (you do the work) reads the same shape as a standing offer (you
508
+ // and a request (you do the work) reads the same shape as a standing offer (you
497
509
  // pay for it) unless you know which slot you landed in.
498
510
  const envelope = data;
499
511
  return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
@@ -683,7 +695,7 @@ export class AgreementClient {
683
695
  list(filters) { return listAgreements(filters, this.creds); }
684
696
  listMine(filters) { return getMyAgreements(filters, this.creds); }
685
697
  get(id) { return getAgreement(id, this.creds); }
686
- create(data) { return createAgreement(data, this.creds); }
698
+ createLink(data) { return createLink(data, this.creds); }
687
699
  revoke(id) { return revokeAgreement(id, this.creds); }
688
700
  fulfill(id) { return fulfillAgreement(id, this.creds); }
689
701
  claimAgreement(id) { return claimAgreement(id, this.creds); }
@@ -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;
@@ -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
  }
@@ -1,5 +1,6 @@
1
1
  import { getBackendUrl } from '../utils/urlUtils.js';
2
2
  import { pollSurfaceError } from '../shared/rateLimit.js';
3
+ import { INBOX_HOST_CLAIMANT_HEADER, instanceIdentity, } from '../instanceIdentity.js';
3
4
  /**
4
5
  * The doorbell, not the door: references addressed to this agent
5
6
  * since its last ack — never content. Flow: inbox → read → act → ack
@@ -24,6 +25,9 @@ export class InboxClient {
24
25
  const headers = {
25
26
  Authorization: `Bearer ${this.operatorKey}`,
26
27
  'Content-Type': 'application/json',
28
+ // This process, not the API. Two fleets of the same agent share
29
+ // one backend task; without this they both wake.
30
+ [INBOX_HOST_CLAIMANT_HEADER]: instanceIdentity(),
27
31
  };
28
32
  if (this.agentId)
29
33
  headers['X-Agent-Id'] = this.agentId;
@@ -23,8 +23,7 @@ export interface PullOffersOptions {
23
23
  since?: string;
24
24
  }
25
25
  export declare function pullOffers(options: PullOffersOptions | undefined, creds: Creds): Promise<Agreement[]>;
26
- export declare function claimOffer(agreementId: string, creds: Creds): Promise<Agreement>;
27
- export interface PublishQuestPayload {
26
+ export interface PublishRequestPayload {
28
27
  /** See PublishOfferPayload.billing. */
29
28
  billing?: 'total' | 'per_task';
30
29
  description: string;
@@ -38,12 +37,12 @@ export interface PublishQuestPayload {
38
37
  /** Broadcast audience: 'everyone' (default, fully public) or 'org' (members of your active org only). */
39
38
  audience?: BroadcastAudience;
40
39
  }
41
- export declare function publishQuest(payload: PublishQuestPayload, creds: Creds): Promise<Agreement>;
42
- export interface PullQuestsOptions {
40
+ export declare function publishRequest(payload: PublishRequestPayload, creds: Creds): Promise<Agreement>;
41
+ export interface PullRequestsOptions {
43
42
  limit?: number;
44
43
  since?: string;
45
44
  }
46
- export declare function pullQuests(options: PullQuestsOptions | undefined, creds: Creds): Promise<Agreement[]>;
45
+ export declare function pullRequests(options: PullRequestsOptions | undefined, creds: Creds): Promise<Agreement[]>;
47
46
  export declare class MarketplaceClient {
48
47
  private creds;
49
48
  /**
@@ -53,7 +52,6 @@ export declare class MarketplaceClient {
53
52
  constructor(operatorKey: string, agentId?: string);
54
53
  publishOffer(payload: PublishOfferPayload): Promise<Agreement>;
55
54
  pullOffers(options?: PullOffersOptions): Promise<Agreement[]>;
56
- claimOffer(agreementId: string): Promise<Agreement>;
57
- publishQuest(payload: PublishQuestPayload): Promise<Agreement>;
58
- pullQuests(options?: PullQuestsOptions): Promise<Agreement[]>;
55
+ publishRequest(payload: PublishRequestPayload): Promise<Agreement>;
56
+ pullRequests(options?: PullRequestsOptions): Promise<Agreement[]>;
59
57
  }
@@ -52,55 +52,37 @@ export async function pullOffers(options, creds) {
52
52
  const data = await res.json().catch(() => null);
53
53
  return data?.['offers'] ?? [];
54
54
  }
55
- export async function claimOffer(agreementId, creds) {
56
- if (!agreementId)
57
- throw new Error('agreementId is required');
58
- assertCreds(creds, 'marketplace offer claim');
59
- const res = await fetch(`${getMarketplaceBaseUrl()}/offers/claim`, {
60
- method: 'POST',
61
- headers: buildHeaders(creds),
62
- body: JSON.stringify({ agreementId }),
63
- });
64
- if (!res.ok) {
65
- const body = await res.text().catch(() => '');
66
- throwApiError(res, body, `Marketplace offer claim failed: ${res.status}`);
67
- }
68
- const data = await res.json().catch(() => null);
69
- if (!data?.['ok'])
70
- throw new Error(data?.['error'] || 'Claim failed');
71
- return shapeAgreement(data['offer']);
72
- }
73
- export async function publishQuest(payload, creds) {
74
- assertCreds(creds, 'quest publish');
75
- const res = await fetch(`${getMarketplaceBaseUrl()}/quests/publish`, {
55
+ export async function publishRequest(payload, creds) {
56
+ assertCreds(creds, 'request publish');
57
+ const res = await fetch(`${getMarketplaceBaseUrl()}/requests/publish`, {
76
58
  method: 'POST',
77
59
  headers: buildHeaders(creds),
78
60
  body: JSON.stringify(payload || {}),
79
61
  });
80
62
  if (!res.ok) {
81
63
  const body = await res.text().catch(() => '');
82
- throwApiError(res, body, `Quest publish failed: ${res.status}`);
64
+ throwApiError(res, body, `Request publish failed: ${res.status}`);
83
65
  }
84
66
  const data = await res.json().catch(() => null);
85
67
  if (!data?.['agreement'])
86
- throw new Error('Quest publish returned no agreement');
68
+ throw new Error('Request publish returned no agreement');
87
69
  return shapeAgreement(data['agreement']);
88
70
  }
89
- export async function pullQuests(options, creds) {
90
- assertCreds(creds, 'quest pull');
71
+ export async function pullRequests(options, creds) {
72
+ assertCreds(creds, 'request pull');
91
73
  const params = new URLSearchParams();
92
74
  if (options?.limit != null)
93
75
  params.set('limit', String(options.limit));
94
76
  if (options?.since)
95
77
  params.set('since', options.since);
96
78
  const qs = params.toString();
97
- const res = await fetch(`${getMarketplaceBaseUrl()}/quests${qs ? `?${qs}` : ''}`, {
79
+ const res = await fetch(`${getMarketplaceBaseUrl()}/requests${qs ? `?${qs}` : ''}`, {
98
80
  method: 'GET',
99
81
  headers: buildHeaders(creds),
100
82
  });
101
83
  if (!res.ok) {
102
84
  const body = await res.text().catch(() => '');
103
- throwApiError(res, body, `Quest pull failed: ${res.status}`);
85
+ throwApiError(res, body, `Request pull failed: ${res.status}`);
104
86
  }
105
87
  const data = await res.json().catch(() => null);
106
88
  return data?.['agreements'] ?? [];
@@ -120,7 +102,6 @@ export class MarketplaceClient {
120
102
  }
121
103
  publishOffer(payload) { return publishOffer(payload, this.creds); }
122
104
  pullOffers(options) { return pullOffers(options, this.creds); }
123
- claimOffer(agreementId) { return claimOffer(agreementId, this.creds); }
124
- publishQuest(payload) { return publishQuest(payload, this.creds); }
125
- pullQuests(options) { return pullQuests(options, this.creds); }
105
+ publishRequest(payload) { return publishRequest(payload, this.creds); }
106
+ pullRequests(options) { return pullRequests(options, this.creds); }
126
107
  }
@@ -86,6 +86,11 @@ export interface ListTasksOptions {
86
86
  limit?: number;
87
87
  /** Filter to tasks assigned to this id. */
88
88
  assignedTo?: string;
89
+ /**
90
+ * Filter to tasks CREATED by this id — what the caller handed out, as
91
+ * opposed to what was handed to it.
92
+ */
93
+ createdBy?: string;
89
94
  }
90
95
  export interface ListTasksResult {
91
96
  tasks: Task[];