@ziggs-ai/api-client 0.8.0 → 0.9.1

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 (49) hide show
  1. package/README.md +10 -0
  2. package/dist/ConnectionManager.d.ts +21 -57
  3. package/dist/ConnectionManager.js +34 -163
  4. package/dist/capabilities/agreements.js +2 -2
  5. package/dist/capabilities/artifacts.js +11 -10
  6. package/dist/capabilities/connections.js +1 -1
  7. package/dist/capabilities/context.js +29 -11
  8. package/dist/capabilities/grants.d.ts +7 -0
  9. package/dist/capabilities/grants.js +9 -2
  10. package/dist/capabilities/index.d.ts +1 -1
  11. package/dist/capabilities/index.js +1 -1
  12. package/dist/capabilities/links.d.ts +0 -8
  13. package/dist/capabilities/links.js +3 -25
  14. package/dist/capabilities/types.d.ts +1 -0
  15. package/dist/capabilities/types.js +7 -2
  16. package/dist/http/AgreementClient.d.ts +67 -16
  17. package/dist/http/AgreementClient.js +161 -29
  18. package/dist/http/ArtifactsClient.d.ts +5 -1
  19. package/dist/http/ArtifactsClient.js +17 -5
  20. package/dist/http/ChatClient.js +5 -2
  21. package/dist/http/ConnectionsClient.js +4 -4
  22. package/dist/http/ContextDiscoveryClient.js +2 -1
  23. package/dist/http/ContextReadClient.d.ts +30 -3
  24. package/dist/http/ContextReadClient.js +63 -17
  25. package/dist/http/GrantsClient.js +2 -1
  26. package/dist/http/InboxClient.d.ts +32 -50
  27. package/dist/http/InboxClient.js +0 -39
  28. package/dist/http/MarketplaceClient.d.ts +0 -1
  29. package/dist/http/MarketplaceClient.js +8 -3
  30. package/dist/http/MessagesClient.js +3 -5
  31. package/dist/http/OrgsClient.js +3 -2
  32. package/dist/http/PaymentsClient.js +3 -5
  33. package/dist/http/TaskClient.d.ts +8 -0
  34. package/dist/http/TaskClient.js +3 -0
  35. package/dist/http/agreementFlows.d.ts +13 -5
  36. package/dist/http/agreementFlows.js +18 -24
  37. package/dist/http/index.d.ts +3 -3
  38. package/dist/http/index.js +1 -1
  39. package/dist/http/operatorHeaders.d.ts +7 -1
  40. package/dist/http/operatorHeaders.js +8 -1
  41. package/dist/index.d.ts +5 -4
  42. package/dist/index.js +4 -3
  43. package/dist/shared/apiError.d.ts +22 -1
  44. package/dist/shared/apiError.js +60 -3
  45. package/dist/shared/rateLimit.d.ts +12 -22
  46. package/dist/shared/rateLimit.js +18 -51
  47. package/dist/types.d.ts +70 -1
  48. package/dist/types.js +39 -1
  49. package/package.json +1 -1
@@ -18,27 +18,6 @@ function webAppOrigin(env) {
18
18
  function inviteShareUrl(env, agreementId) {
19
19
  return `${webAppOrigin(env)}/connect/${agreementId}`;
20
20
  }
21
- /**
22
- * ZIG-670 — link-shaped summaries, not raw agreement documents: the money
23
- * block, approvals array, and Mongo internals are noise on a trust
24
- * relationship. ZIG-956 moved this into the shared layer so the MCP mutations
25
- * return it too (they used to leak the raw agreement doc).
26
- */
27
- export function linkSummary(a) {
28
- return {
29
- agreementId: a.agreementId,
30
- status: a.status,
31
- proposalStatus: a.proposalStatus,
32
- parties: {
33
- creatorAgent: a.parties?.creatorAgent ?? null,
34
- providerAgent: a.parties?.providerAgent ?? null,
35
- creator: a.parties?.creator ?? null,
36
- proposedTo: a.parties?.proposedTo ?? null,
37
- },
38
- ...(a.description ? { description: a.description } : {}),
39
- createdAt: a.createdAt,
40
- };
41
- }
42
21
  /**
43
22
  * A link is reach-only — the follow-up move differs by surface tool names.
44
23
  *
@@ -106,7 +85,7 @@ export const createLinkInviteCapability = {
106
85
  message: env.surface === 'mcp'
107
86
  ? `Open link invite created, ${seatNote}. Give the human shareUrl and nothing else — it is the whole invite. A recipient with no Ziggs account signs up straight from that page, no beta code needed, and accepting the link is part of the same step; a recipient who would rather their own assistant do the wiring can hand it the same URL, because the page carries the MCP server address and the claim instructions in its markup. Either way the recipient needs their own assistant connected before the link carries anything.`
108
87
  : `Open link invite created, ${seatNote}. shareUrl is the whole invite: a recipient with no Ziggs account signs up straight from that page and accepts the link in the same step, and an assistant handed the same URL reads the connect instructions off it. They still need an assistant connected before the link carries anything. No agent id needed on either side. Revoke with agreement_revoke to disable.`,
109
- agreement: linkSummary(agreement),
88
+ agreement,
110
89
  };
111
90
  },
112
91
  sdkOptions: { isAgreementCreation: true },
@@ -136,12 +115,11 @@ export const listLinksCapability = {
136
115
  engagementKind: 'link',
137
116
  ...(status === 'all' ? {} : { status }),
138
117
  }, fullCreds(env));
139
- const summaries = links.map(linkSummary);
140
118
  const hasActive = links.some((a) => a.status === 'active');
141
119
  return {
142
- count: summaries.length,
120
+ count: links.length,
143
121
  status,
144
- links: summaries,
122
+ links,
145
123
  ...(hasActive ? { nextSteps: linkIsReachOnly(env) } : {}),
146
124
  };
147
125
  },
@@ -43,6 +43,7 @@ export interface CapabilityEnv {
43
43
  creds: {
44
44
  operatorKey: string;
45
45
  agentId?: string;
46
+ laneId?: string;
46
47
  };
47
48
  /** SDK runner base-URL override (BACKEND_URL / ZIGGS_BACKEND_URL). */
48
49
  baseUrl?: string;
@@ -1,11 +1,13 @@
1
1
  /** Creds for impersonated calls; adapters guarantee agentId when needsAgentId. */
2
2
  export function fullCreds(env) {
3
- const { operatorKey, agentId } = env.creds;
3
+ const { operatorKey, agentId, laneId } = env.creds;
4
4
  if (!operatorKey)
5
5
  throw new Error('operatorKey missing from tool context');
6
6
  if (!agentId)
7
7
  throw new Error('agentId missing from tool context');
8
- return { operatorKey, agentId };
8
+ // ZIG-1092 — the lane rides along so every Creds-based client sends
9
+ // X-Ziggs-Lane without each capability having to remember to.
10
+ return { operatorKey, agentId, ...(laneId ? { laneId } : {}) };
9
11
  }
10
12
  /**
11
13
  * Re-throw a client error with a capability-level prefix, preserving the HTTP
@@ -19,6 +21,9 @@ export function rethrowWithContext(error, prefix) {
19
21
  wrapped.status = e.status;
20
22
  if (e.body !== undefined)
21
23
  wrapped.body = e.body;
24
+ // ZIG-1124 — keep the machine code so toolError can classify without prose.
25
+ if (typeof e.code === 'string' && e.code)
26
+ wrapped.code = e.code;
22
27
  wrapped['cause'] = error;
23
28
  throw wrapped;
24
29
  }
@@ -1,6 +1,5 @@
1
1
  import 'dotenv/config';
2
2
  import { type Creds, type Agreement, type EngagementKind, type BroadcastAudience } from '../types.js';
3
- export type PlanReviewTiming = 'with_proposal' | 'before_execution';
4
3
  /**
5
4
  * Shared proposal terms. When `engagementKind` is omitted the server defaults to `service`.
6
5
  * For an open buyer-broadcast quest, set `proposedTo` to a broadcast sentinel —
@@ -22,7 +21,6 @@ export interface ProposeTerms {
22
21
  billing?: 'total' | 'per_task';
23
22
  agreementDescription?: string;
24
23
  parentAgreementId?: string;
25
- parentTaskId?: string;
26
24
  /**
27
25
  * Who does the work. Required on direct proposals (your own id = you offer;
28
26
  * the proposedTo id = you commission the recipient); forbidden on
@@ -30,9 +28,6 @@ export interface ProposeTerms {
30
28
  * side — there is no payer input.
31
29
  */
32
30
  providerId?: string;
33
- plan?: unknown;
34
- planReviewTiming?: PlanReviewTiming;
35
- requireMidWorkPlanAck?: boolean;
36
31
  /** Defaults to `service` on the server when omitted. Set `hire` for representation contracts. */
37
32
  engagementKind?: EngagementKind;
38
33
  idempotencyKey?: string;
@@ -51,6 +46,33 @@ export interface ProposeDirectInput extends ProposeTerms {
51
46
  export type ProposeBroadcastInput = Omit<ProposeDirectInput, 'proposedTo'> & {
52
47
  audience?: BroadcastAudience;
53
48
  };
49
+ /**
50
+ * A trust link's agreement, as every caller sees it.
51
+ *
52
+ * A link is reach, not commerce: it carries no money, no escrow, no execution
53
+ * state and no approvals ledger. Handing the raw document over anyway put Mongo
54
+ * bookkeeping in front of an LLM, which is what ZIG-957 forbade.
55
+ *
56
+ * Lives here rather than in `capabilities/links.ts` because this is where the
57
+ * rule is applied (ZIG-1111); that module re-exports it so the public name is
58
+ * unchanged.
59
+ */
60
+ export declare function linkSummary(a: Agreement): Record<string, unknown>;
61
+ /**
62
+ * Every agreement document this client parses passes through here.
63
+ *
64
+ * The rule — a link is returned as its summary, anything else verbatim — used to
65
+ * be written out at six call sites across the SDK runner, the MCP tools and the
66
+ * capability layer, and three verbs never got it: `agreement_counter`,
67
+ * `agreement_fulfill` and `agreement_subcontract` returned the raw document
68
+ * (ZIG-1111). Applying it at the parse boundary means a new verb inherits the
69
+ * rule instead of remembering to opt in, and no surface can word its own verdict.
70
+ *
71
+ * Typed as `Agreement` on the way out: every key the summary keeps IS an
72
+ * Agreement field, so this narrows a document rather than returning a different
73
+ * shape.
74
+ */
75
+ export declare function shapeAgreement(a: Agreement): Agreement;
54
76
  /** @deprecated Alias for {@link ProposeDirectInput}. */
55
77
  export type ProposeAgreementData = ProposeDirectInput;
56
78
  export declare function proposeAgreement(proposalData: ProposeDirectInput, creds: Creds): Promise<Agreement>;
@@ -67,16 +89,12 @@ export interface DelegateAgreementData {
67
89
  executorId: string;
68
90
  chatId: string;
69
91
  parentAgreementId: string;
70
- parentTaskId?: string;
71
92
  price?: number;
72
93
  lifecycle?: string;
73
94
  expiresAt?: string;
74
95
  maxExecutions?: number;
75
96
  agreementDescription?: string;
76
97
  payerId?: string;
77
- plan?: unknown;
78
- planReviewTiming?: PlanReviewTiming;
79
- requireMidWorkPlanAck?: boolean;
80
98
  idempotencyKey?: string;
81
99
  }
82
100
  export declare function delegateAgreement(proposalData: DelegateAgreementData, creds: Creds): Promise<Agreement>;
@@ -91,7 +109,29 @@ export declare function delegateAgreement(proposalData: DelegateAgreementData, c
91
109
  export declare function respondToAgreement(agreementId: string, action: 'approve' | 'reject', creds: Creds, opts?: {
92
110
  ownerUserId?: string | null;
93
111
  agreement?: Agreement | null;
112
+ /** Where to send the human when the decision is theirs (ZIG-1087). */
113
+ appUrl?: string;
94
114
  }): Promise<Agreement>;
115
+ /**
116
+ * The approval facts a "who decides this?" question is answered from: the party
117
+ * ids still owing a decision, plus the named responder slot.
118
+ *
119
+ * Split out (ZIG-1087) so the inbox card and the respond call answer that
120
+ * question with ONE rule. The card had no rule at all — it offered the respond
121
+ * tool for every pending proposal, including the ones only the human can
122
+ * answer, and the agent found out by 403.
123
+ */
124
+ export interface PendingApprovalFacts {
125
+ /** Party ids with a pending entry on the approvals ledger. */
126
+ pendingPartyIds: readonly string[];
127
+ /** The direct responder slot, when the proposal names one. */
128
+ proposedTo?: string | null;
129
+ }
130
+ /**
131
+ * Which of `candidateIds` holds the pending approval slot, first match wins —
132
+ * so pass them in preference order (agent id before owner principal).
133
+ */
134
+ export declare function resolvePendingApprovalPartyId(facts: PendingApprovalFacts, candidateIds: ReadonlyArray<string | null | undefined>): string | null;
95
135
  /**
96
136
  * ZIG-524 — resolve which approvals.partyId the current operator may submit.
97
137
  * Checks pending ledger entries against impersonated agent id and owner principal.
@@ -116,10 +156,6 @@ export interface CounterAgreementData {
116
156
  lifecycle?: string;
117
157
  maxExecutions?: number;
118
158
  description?: string;
119
- /** Override plan `{ steps: [...] }`; omit to copy from original proposal task */
120
- plan?: unknown;
121
- planReviewTiming?: PlanReviewTiming;
122
- requireMidWorkPlanAck?: boolean;
123
159
  }
124
160
  export declare function counterAgreement(agreementId: string, counter: CounterAgreementData, creds: Creds): Promise<Agreement>;
125
161
  export declare function getAgreementStatus(agreementId: string, creds: Creds): Promise<unknown | null>;
@@ -143,6 +179,7 @@ export interface GetMyAgreementsFilters {
143
179
  partyOnly?: boolean;
144
180
  }
145
181
  export declare function getMyAgreements(filters: GetMyAgreementsFilters | undefined, creds: Creds): Promise<Agreement[]>;
182
+ /** One agreement, shaped: a link comes back as its summary. */
146
183
  export declare function getAgreement(agreementId: string, creds: Creds): Promise<Agreement | null>;
147
184
  export interface CreateAgreementBody {
148
185
  proposedToId?: string;
@@ -172,6 +209,8 @@ export declare function fulfillAgreement(agreementId: string, creds: Creds): Pro
172
209
  ok: boolean;
173
210
  agreement: Agreement;
174
211
  }>;
212
+ /** What an open-broadcast claim turned out to be. ⚠️ SYNC: backend AgreementOpenService. */
213
+ export type ClaimedKind = 'link' | 'offer' | 'quest' | 'hand-off';
175
214
  /**
176
215
  * Claim an open agreement (ZIG-524 phase 2 / ZIG-525):
177
216
  * - open link invite (`engagementKind: link`, proposedTo everyone)
@@ -184,8 +223,17 @@ export declare function fulfillAgreement(agreementId: string, creds: Creds): Pro
184
223
  export declare function claimAgreement(agreementId: string, creds: Creds): Promise<{
185
224
  ok: boolean;
186
225
  agreement: Agreement;
226
+ kind?: ClaimedKind;
187
227
  }>;
188
- export type ChatLinkType = 'origin' | 'mention' | 'delegation' | 'join';
228
+ /**
229
+ * Link types a caller may ASK for — the server's `CALLER_LINK_TYPES`, not its
230
+ * full `LINK_TYPES`. `space` is deliberately absent: an agreement space gets
231
+ * exactly one system-minted room, and letting a request name that type would
232
+ * burn the root's single slot on an unrelated chat. Shorter than the server
233
+ * union on purpose, which is why this list carries the reason with it.
234
+ */
235
+ export declare const CHAT_LINK_TYPES: readonly ["origin", "mention", "delegation", "join"];
236
+ export type ChatLinkType = (typeof CHAT_LINK_TYPES)[number];
189
237
  export declare function linkAgreementToChat(agreementId: string, chatId: string, linkType: ChatLinkType | undefined, creds: Creds): Promise<unknown | null>;
190
238
  export declare function getChatsForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
191
239
  export declare function joinAgreement(agreementId: string, creds: Creds): Promise<{
@@ -193,10 +241,12 @@ export declare function joinAgreement(agreementId: string, creds: Creds): Promis
193
241
  agentId: string | null;
194
242
  isNew: boolean;
195
243
  }>;
196
- export type ArtifactLinkType = 'produced' | 'referenced';
244
+ export declare const ARTIFACT_LINK_TYPES: readonly ["produced", "referenced"];
245
+ export type ArtifactLinkType = (typeof ARTIFACT_LINK_TYPES)[number];
197
246
  export declare function linkArtifactToAgreement(agreementId: string, artifactId: string, linkType: ArtifactLinkType | undefined, creds: Creds): Promise<unknown | null>;
198
247
  export declare function getArtifactsForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
199
- export type UserRole = 'payer' | 'provider' | 'participant' | 'observer';
248
+ export declare const AGREEMENT_USER_ROLES: readonly ["payer", "provider", "participant", "observer"];
249
+ export type UserRole = (typeof AGREEMENT_USER_ROLES)[number];
200
250
  export declare function linkUserToAgreement(agreementId: string, userId: string, role: UserRole, creds: Creds): Promise<unknown | null>;
201
251
  export declare function getUsersForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
202
252
  export declare class AgreementClient {
@@ -232,6 +282,7 @@ export declare class AgreementClient {
232
282
  claimAgreement(id: string): Promise<{
233
283
  ok: boolean;
234
284
  agreement: Agreement;
285
+ kind?: ClaimedKind;
235
286
  }>;
236
287
  linkToChat(id: string, chatId: string, linkType?: ChatLinkType): Promise<unknown>;
237
288
  listChats(id: string): Promise<unknown[]>;
@@ -14,6 +14,9 @@ function buildHeaders(creds) {
14
14
  'content-type': 'application/json',
15
15
  Authorization: `Bearer ${creds.operatorKey}`,
16
16
  'X-Agent-Id': creds.agentId,
17
+ // ZIG-1092 — the wake's lane, so the backend can fence this call to the
18
+ // engagement it belongs to rather than the agent's whole authority.
19
+ ...(creds.laneId ? { 'X-Ziggs-Lane': creds.laneId } : {}),
17
20
  };
18
21
  }
19
22
  function assertCreds(creds, op) {
@@ -22,6 +25,54 @@ function assertCreds(creds, op) {
22
25
  if (!creds?.agentId)
23
26
  throw new Error(`agentId is required for ${op}`);
24
27
  }
28
+ /**
29
+ * A trust link's agreement, as every caller sees it.
30
+ *
31
+ * A link is reach, not commerce: it carries no money, no escrow, no execution
32
+ * state and no approvals ledger. Handing the raw document over anyway put Mongo
33
+ * bookkeeping in front of an LLM, which is what ZIG-957 forbade.
34
+ *
35
+ * Lives here rather than in `capabilities/links.ts` because this is where the
36
+ * rule is applied (ZIG-1111); that module re-exports it so the public name is
37
+ * unchanged.
38
+ */
39
+ export function linkSummary(a) {
40
+ return {
41
+ agreementId: a.agreementId,
42
+ // Kept deliberately: callers branch on this, and a summary that hides what
43
+ // kind of thing it describes breaks the code it is meant to protect.
44
+ engagementKind: a.engagementKind,
45
+ status: a.status,
46
+ proposalStatus: a.proposalStatus,
47
+ 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,
52
+ },
53
+ ...(a.description ? { description: a.description } : {}),
54
+ // Seat bookkeeping on an open invite — link state, not commerce.
55
+ ...(a.linkInvite ? { linkInvite: a.linkInvite } : {}),
56
+ createdAt: a.createdAt,
57
+ };
58
+ }
59
+ /**
60
+ * Every agreement document this client parses passes through here.
61
+ *
62
+ * The rule — a link is returned as its summary, anything else verbatim — used to
63
+ * be written out at six call sites across the SDK runner, the MCP tools and the
64
+ * capability layer, and three verbs never got it: `agreement_counter`,
65
+ * `agreement_fulfill` and `agreement_subcontract` returned the raw document
66
+ * (ZIG-1111). Applying it at the parse boundary means a new verb inherits the
67
+ * rule instead of remembering to opt in, and no surface can word its own verdict.
68
+ *
69
+ * Typed as `Agreement` on the way out: every key the summary keeps IS an
70
+ * Agreement field, so this narrows a document rather than returning a different
71
+ * shape.
72
+ */
73
+ export function shapeAgreement(a) {
74
+ return a.engagementKind === 'link' ? linkSummary(a) : a;
75
+ }
25
76
  export async function proposeAgreement(proposalData, creds) {
26
77
  if (!proposalData)
27
78
  throw new Error('Proposal data is required for proposal creation');
@@ -45,7 +96,7 @@ export async function proposeAgreement(proposalData, creds) {
45
96
  if (!data?.['agreement']) {
46
97
  throw new Error('Invalid response: expected { agreement } from POST /agreements/proposals');
47
98
  }
48
- return data['agreement'];
99
+ return shapeAgreement(data['agreement']);
49
100
  }
50
101
  /** Propose a contract to one party (user or agent). Server defaults `engagementKind` to `service`. */
51
102
  export async function proposeDirectTo(input, creds) {
@@ -93,7 +144,7 @@ export async function delegateAgreement(proposalData, creds) {
93
144
  if (!data?.['agreement']) {
94
145
  throw new Error('Invalid response: expected { agreement } from POST /agreements/:parentAgreementId/delegations');
95
146
  }
96
- return data['agreement'];
147
+ return shapeAgreement(data['agreement']);
97
148
  }
98
149
  /**
99
150
  * Approve or reject a pending agreement (ZIG-524 canonical client path).
@@ -107,7 +158,10 @@ export async function respondToAgreement(agreementId, action, creds, opts = {})
107
158
  if (!agreementId || !action)
108
159
  throw new Error('Agreement ID and action are required for proposal response');
109
160
  assertCreds(creds, 'proposal response');
110
- const agreement = opts.agreement ?? (await getAgreement(agreementId, creds));
161
+ // Unshaped on purpose: the approvals ledger below is exactly what the link
162
+ // summary drops, and resolving the caller's slot from a summary would fail
163
+ // every link approval with "no pending approval entry".
164
+ const agreement = opts.agreement ?? (await getAgreementDocument(agreementId, creds));
111
165
  if (!agreement) {
112
166
  throw new Error(`Agreement ${agreementId} not found`);
113
167
  }
@@ -135,28 +189,54 @@ export async function respondToAgreement(agreementId, action, creds, opts = {})
135
189
  else if (!partyId) {
136
190
  throw new Error(`No pending approval entry for this operator on agreement ${agreementId}`);
137
191
  }
192
+ // ZIG-1087 — the slot resolved to the principal, not to us. Every call from
193
+ // this client impersonates `creds.agentId` (X-Agent-Id on every request), and
194
+ // `PUT /approvals/:partyId` takes a decision only from the party itself, so
195
+ // sending this would 403 with "You can only submit your own approval" —
196
+ // a condition, from which the caller cannot tell that no retry will ever
197
+ // work. Withholding consent from delegates is deliberate; being silent about
198
+ // it is not.
199
+ if (partyId && partyId !== creds.agentId) {
200
+ throw new Error(`Agreement ${agreementId} is waiting on ${partyId} — your principal, not you. ` +
201
+ `Consent is the human's to give: a delegate can submit its own approval slot and never its principal's, ` +
202
+ `so there is nothing to retry here and no tool that changes it. ` +
203
+ `Ask your human to approve or reject it${opts.appUrl ? ` at ${opts.appUrl}` : ' in the Ziggs app, under Agreements'}, ` +
204
+ `then read the agreement again to see the outcome.`);
205
+ }
138
206
  return approveAgreementAsParty(agreementId, partyId, action === 'approve' ? 'approved' : 'rejected', creds);
139
207
  }
140
208
  /**
141
- * ZIG-524 — resolve which approvals.partyId the current operator may submit.
142
- * Checks pending ledger entries against impersonated agent id and owner principal.
209
+ * Which of `candidateIds` holds the pending approval slot, first match wins —
210
+ * so pass them in preference order (agent id before owner principal).
143
211
  */
144
- export function resolveMyPendingApprovalPartyId(agreement, opts) {
145
- const actorIds = [opts.agentId, opts.ownerUserId].filter((id) => typeof id === 'string' && id.length > 0);
146
- const pending = (agreement.approvals ?? []).filter((a) => a.status === 'pending' || a.status === 'PENDING');
212
+ export function resolvePendingApprovalPartyId(facts, candidateIds) {
213
+ const actorIds = candidateIds.filter((id) => typeof id === 'string' && id.length > 0);
147
214
  for (const id of actorIds) {
148
- const hit = pending.find((a) => a.partyId === id);
149
- if (hit)
150
- return hit.partyId;
215
+ if (facts.pendingPartyIds.includes(id))
216
+ return id;
151
217
  }
152
- const proposedTo = agreement.parties?.proposedTo;
218
+ // A row with no pending ledger entries is a legacy one (approvals[] predates
219
+ // ZIG-244), and there the named responder slot IS the decision.
220
+ const proposedTo = facts.proposedTo ?? null;
153
221
  if (proposedTo &&
154
222
  actorIds.includes(proposedTo) &&
155
- (pending.length === 0 || pending.some((a) => a.partyId === proposedTo))) {
223
+ facts.pendingPartyIds.length === 0) {
156
224
  return proposedTo;
157
225
  }
158
226
  return null;
159
227
  }
228
+ /**
229
+ * ZIG-524 — resolve which approvals.partyId the current operator may submit.
230
+ * Checks pending ledger entries against impersonated agent id and owner principal.
231
+ */
232
+ export function resolveMyPendingApprovalPartyId(agreement, opts) {
233
+ return resolvePendingApprovalPartyId({
234
+ pendingPartyIds: (agreement.approvals ?? [])
235
+ .filter((a) => a.status === 'pending' || a.status === 'PENDING')
236
+ .map((a) => a.partyId),
237
+ proposedTo: agreement.parties?.proposedTo ?? null,
238
+ }, [opts.agentId, opts.ownerUserId]);
239
+ }
160
240
  /**
161
241
  * Record this party's approval decision on an agreement
162
242
  * (`PUT /agreements/:agreementId/approvals/:partyId`).
@@ -184,7 +264,7 @@ export async function approveAgreementAsParty(agreementId, partyId, status, cred
184
264
  if (!data?.['agreement']) {
185
265
  throw new Error('Invalid response: expected { agreement } from PUT /agreements/:id/approvals/:partyId');
186
266
  }
187
- return data['agreement'];
267
+ return shapeAgreement(data['agreement']);
188
268
  }
189
269
  export async function counterAgreement(agreementId, counter, creds) {
190
270
  if (!agreementId)
@@ -203,7 +283,7 @@ export async function counterAgreement(agreementId, counter, creds) {
203
283
  if (!data?.['agreement']) {
204
284
  throw new Error('Invalid response: expected { agreement } from /agreements/:id/counter');
205
285
  }
206
- return data['agreement'];
286
+ return shapeAgreement(data['agreement']);
207
287
  }
208
288
  export async function getAgreementStatus(agreementId, creds) {
209
289
  if (!agreementId)
@@ -232,8 +312,7 @@ export async function listAgreements(filters = {}, creds) {
232
312
  assertCreds(creds, 'list agreements');
233
313
  // ZIG-699 — return an empty array only for a genuine empty 200 result. Any
234
314
  // failure (non-2xx / network) throws so the MCP tool reports a real error
235
- // instead of "you have no agreements". The status stays whitespace-delimited
236
- // so the tool's toolError/classifyToolError maps it to a stable code.
315
+ // instead of "you have no agreements". ZIG-1124 — HTTP failures are ApiError.
237
316
  // Canonical query path: GET /agreements?scope=&status=&engagementKind=&...
238
317
  const url = new URL(getAgreementBaseUrl());
239
318
  if (filters.status)
@@ -255,10 +334,12 @@ export async function listAgreements(filters = {}, creds) {
255
334
  if (!res.ok) {
256
335
  const body = await res.text().catch(() => '');
257
336
  runtimeLog.warn('AgreementClient', `⚠️ List agreements failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
258
- throw new Error(`GET /agreements ${res.status} ${body?.slice(0, 200)}`);
337
+ throwApiError(res, body, `GET /agreements failed: ${res.status}`);
259
338
  }
260
339
  const data = await res.json().catch(() => null);
261
- return Array.isArray(data?.['agreements']) ? data['agreements'] : [];
340
+ return Array.isArray(data?.['agreements'])
341
+ ? data['agreements'].map(shapeAgreement)
342
+ : [];
262
343
  }
263
344
  export async function getMyAgreements(filters = {}, creds) {
264
345
  assertCreds(creds, 'get my agreements');
@@ -287,20 +368,29 @@ export async function getMyAgreements(filters = {}, creds) {
287
368
  if (!res.ok) {
288
369
  const body = await res.text().catch(() => '');
289
370
  runtimeLog.warn('AgreementClient', `⚠️ Get my agreements failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
290
- throw new Error(`GET /agreements?scope=mine ${res.status} ${body?.slice(0, 200)}`);
371
+ throwApiError(res, body, `GET /agreements?scope=mine failed: ${res.status}`);
291
372
  }
292
373
  const data = await res.json().catch(() => null);
293
- return Array.isArray(data?.['agreements']) ? data['agreements'] : [];
374
+ return Array.isArray(data?.['agreements'])
375
+ ? data['agreements'].map(shapeAgreement)
376
+ : [];
294
377
  }
295
- export async function getAgreement(agreementId, creds) {
378
+ /**
379
+ * The full agreement document, unshaped — for this client's OWN reads.
380
+ *
381
+ * `respondToAgreement` resolves which approvals slot the caller may fill, and
382
+ * `claimOpenAgreement` routes on the broadcast's open side; both need fields the
383
+ * link summary deliberately drops. Callers outside this module get the shaped
384
+ * `getAgreement`.
385
+ */
386
+ async function getAgreementDocument(agreementId, creds) {
296
387
  if (!agreementId)
297
388
  return null;
298
389
  assertCreds(creds, 'get agreement');
299
390
  // ZIG-698 — a genuine 404 (and 200-with-no-agreement) is a real "not found"
300
391
  // and returns null. Every other failure (403/5xx/network) must throw so the
301
392
  // caller can tell "does not exist" from "could not fetch" instead of a 403
302
- // masquerading as not-found. The MCP tool classifies the thrown status via
303
- // toolError; the message keeps the status whitespace-delimited for that.
393
+ // masquerading as not-found. ZIG-1124 — thrown as ApiError for toolError.
304
394
  let res;
305
395
  try {
306
396
  res = await fetch(`${getAgreementBaseUrl()}/${agreementId}`, {
@@ -317,11 +407,16 @@ export async function getAgreement(agreementId, creds) {
317
407
  if (!res.ok) {
318
408
  const body = await res.text().catch(() => '');
319
409
  runtimeLog.warn('AgreementClient', `⚠️ Get agreement failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
320
- throw new Error(`GET /agreements/${agreementId} ${res.status} ${body?.slice(0, 200)}`);
410
+ throwApiError(res, body, `GET /agreements/${agreementId} failed: ${res.status}`);
321
411
  }
322
412
  const data = await res.json().catch(() => null);
323
413
  return data?.['agreement'] ?? null;
324
414
  }
415
+ /** One agreement, shaped: a link comes back as its summary. */
416
+ export async function getAgreement(agreementId, creds) {
417
+ const agreement = await getAgreementDocument(agreementId, creds);
418
+ return agreement ? shapeAgreement(agreement) : null;
419
+ }
325
420
  export async function createAgreement(body, creds) {
326
421
  if (!body)
327
422
  throw new Error('Body is required for agreement creation');
@@ -340,7 +435,8 @@ export async function createAgreement(body, creds) {
340
435
  if (!data?.['agreement']) {
341
436
  throw new Error('Invalid response: expected { ok, agreement } from POST /agreements');
342
437
  }
343
- return data;
438
+ const envelope = data;
439
+ return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
344
440
  }
345
441
  export async function revokeAgreement(agreementId, creds) {
346
442
  if (!agreementId)
@@ -360,7 +456,8 @@ export async function revokeAgreement(agreementId, creds) {
360
456
  if (!data?.['agreement']) {
361
457
  throw new Error('Invalid response: expected { ok, agreement } from DELETE /agreements/:id');
362
458
  }
363
- return data;
459
+ const envelope = data;
460
+ return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
364
461
  }
365
462
  /** ZIG-832: mark an agreement fulfilled (complete). A provider closing its own
366
463
  * delivered work — party-gated server-side. */
@@ -380,7 +477,8 @@ export async function fulfillAgreement(agreementId, creds) {
380
477
  if (!data?.['agreement']) {
381
478
  throw new Error('Invalid response: expected { ok, agreement } from POST /agreements/:id/fulfill');
382
479
  }
383
- return data;
480
+ const envelope = data;
481
+ return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
384
482
  }
385
483
  /**
386
484
  * Claim an open agreement (ZIG-524 phase 2 / ZIG-525):
@@ -404,8 +502,29 @@ export async function claimAgreement(agreementId, creds) {
404
502
  if (!data?.['agreement']) {
405
503
  throw new Error('Invalid response: expected { ok, agreement } from POST /agreements/:id/claim');
406
504
  }
407
- return data;
505
+ // ZIG-1155: the route reports which kind of broadcast this turned out to be.
506
+ // A caller cannot work it out from the row — post-claim no sentinel is left,
507
+ // and a quest (you do the work) reads the same shape as a standing offer (you
508
+ // pay for it) unless you know which slot you landed in.
509
+ const envelope = data;
510
+ return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
408
511
  }
512
+ // ---------------------------------------------------------------------------
513
+ // Chat links
514
+ // ---------------------------------------------------------------------------
515
+ /**
516
+ * Link types a caller may ASK for — the server's `CALLER_LINK_TYPES`, not its
517
+ * full `LINK_TYPES`. `space` is deliberately absent: an agreement space gets
518
+ * exactly one system-minted room, and letting a request name that type would
519
+ * burn the root's single slot on an unrelated chat. Shorter than the server
520
+ * union on purpose, which is why this list carries the reason with it.
521
+ */
522
+ export const CHAT_LINK_TYPES = [
523
+ 'origin',
524
+ 'mention',
525
+ 'delegation',
526
+ 'join',
527
+ ];
409
528
  export async function linkAgreementToChat(agreementId, chatId, linkType = 'mention', creds) {
410
529
  if (!agreementId || !chatId)
411
530
  return null;
@@ -468,6 +587,10 @@ export async function joinAgreement(agreementId, creds) {
468
587
  }
469
588
  return data;
470
589
  }
590
+ // ---------------------------------------------------------------------------
591
+ // Artifact links
592
+ // ---------------------------------------------------------------------------
593
+ export const ARTIFACT_LINK_TYPES = ['produced', 'referenced'];
471
594
  export async function linkArtifactToAgreement(agreementId, artifactId, linkType = 'produced', creds) {
472
595
  if (!agreementId || !artifactId)
473
596
  return null;
@@ -512,6 +635,15 @@ export async function getArtifactsForAgreement(agreementId, creds) {
512
635
  return [];
513
636
  }
514
637
  }
638
+ // ---------------------------------------------------------------------------
639
+ // User links
640
+ // ---------------------------------------------------------------------------
641
+ export const AGREEMENT_USER_ROLES = [
642
+ 'payer',
643
+ 'provider',
644
+ 'participant',
645
+ 'observer',
646
+ ];
515
647
  export async function linkUserToAgreement(agreementId, userId, role, creds) {
516
648
  if (!agreementId || !userId || !role)
517
649
  return null;
@@ -62,11 +62,15 @@ export declare function artifactScopeForSession(sessionId: string): {
62
62
  export declare class ArtifactsClient {
63
63
  private readonly operatorKey;
64
64
  private readonly agentId?;
65
+ private readonly laneId?;
65
66
  /**
66
67
  * @param operatorKey Agent-scoped or fleet operator key.
67
68
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
69
+ * @param laneId ZIG-1092 — the wake's lane (chat id, or `agrn-<agreementId>`),
70
+ * sent as X-Ziggs-Lane so `authoredBy=me` is fenced to this engagement
71
+ * instead of returning every customer's deliverables in one call.
68
72
  */
69
- constructor(operatorKey: string, agentId?: string);
73
+ constructor(operatorKey: string, agentId?: string, laneId?: string);
70
74
  list(q: ListArtifactsQuery, opts?: ListArtifactsOptions): Promise<ListArtifactsResult>;
71
75
  /**
72
76
  * Record an agent thought as an `agent-private` artifact. Replaces the
@@ -28,15 +28,20 @@ export function artifactScopeForSession(sessionId) {
28
28
  export class ArtifactsClient {
29
29
  operatorKey;
30
30
  agentId;
31
+ laneId;
31
32
  /**
32
33
  * @param operatorKey Agent-scoped or fleet operator key.
33
34
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
35
+ * @param laneId ZIG-1092 — the wake's lane (chat id, or `agrn-<agreementId>`),
36
+ * sent as X-Ziggs-Lane so `authoredBy=me` is fenced to this engagement
37
+ * instead of returning every customer's deliverables in one call.
34
38
  */
35
- constructor(operatorKey, agentId) {
39
+ constructor(operatorKey, agentId, laneId) {
36
40
  if (!operatorKey)
37
41
  throw new Error('ArtifactsClient: operatorKey is required');
38
42
  this.operatorKey = operatorKey;
39
43
  this.agentId = agentId;
44
+ this.laneId = laneId;
40
45
  }
41
46
  async list(q, opts = {}) {
42
47
  const selectors = [q.chatId, q.agreementId, q.taskId, q.authoredBy].filter(Boolean);
@@ -178,12 +183,19 @@ export class ArtifactsClient {
178
183
  */
179
184
  _assertScopeXor(input) {
180
185
  if (input.chatId && input.agreementId) {
181
- throw new Error('ArtifactsClient.write: pass at most one of chatId or agreementId');
186
+ // ZIG-1075: the refusal names the recovery, because this is the one gate
187
+ // for every caller — the exposed tool surfaces inherit it rather than each
188
+ // wording their own, and a caller that used to have chatId silently
189
+ // dropped here now learns what to do instead. Wording matters: a bare
190
+ // "pick one" at the last step of a finished task is what the drop was
191
+ // added to avoid (ZIG-924 dogfood).
192
+ throw new Error('pass at most one of chatId or agreementId — a deliverable under an ' +
193
+ 'agreement wants agreementId alone (its parties see it); use chatId ' +
194
+ 'only for a chat-scoped note. To put it in both places, record it ' +
195
+ 'with agreementId, then attach it to the chat.');
182
196
  }
183
197
  }
184
198
  _headers() {
185
- return buildOperatorHeaders(this.operatorKey, this.agentId, {
186
- 'content-type': 'application/json',
187
- });
199
+ return buildOperatorHeaders(this.operatorKey, this.agentId, { 'content-type': 'application/json' }, this.laneId);
188
200
  }
189
201
  }