@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.
- package/README.md +10 -0
- package/dist/ConnectionManager.d.ts +21 -57
- package/dist/ConnectionManager.js +34 -163
- package/dist/capabilities/agreements.js +2 -2
- package/dist/capabilities/artifacts.js +11 -10
- package/dist/capabilities/connections.js +1 -1
- package/dist/capabilities/context.js +29 -11
- package/dist/capabilities/grants.d.ts +7 -0
- package/dist/capabilities/grants.js +9 -2
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/capabilities/index.js +1 -1
- package/dist/capabilities/links.d.ts +0 -8
- package/dist/capabilities/links.js +3 -25
- package/dist/capabilities/types.d.ts +1 -0
- package/dist/capabilities/types.js +7 -2
- package/dist/http/AgreementClient.d.ts +67 -16
- package/dist/http/AgreementClient.js +161 -29
- package/dist/http/ArtifactsClient.d.ts +5 -1
- package/dist/http/ArtifactsClient.js +17 -5
- package/dist/http/ChatClient.js +5 -2
- package/dist/http/ConnectionsClient.js +4 -4
- package/dist/http/ContextDiscoveryClient.js +2 -1
- package/dist/http/ContextReadClient.d.ts +30 -3
- package/dist/http/ContextReadClient.js +63 -17
- package/dist/http/GrantsClient.js +2 -1
- package/dist/http/InboxClient.d.ts +32 -50
- package/dist/http/InboxClient.js +0 -39
- package/dist/http/MarketplaceClient.d.ts +0 -1
- package/dist/http/MarketplaceClient.js +8 -3
- package/dist/http/MessagesClient.js +3 -5
- package/dist/http/OrgsClient.js +3 -2
- package/dist/http/PaymentsClient.js +3 -5
- package/dist/http/TaskClient.d.ts +8 -0
- package/dist/http/TaskClient.js +3 -0
- package/dist/http/agreementFlows.d.ts +13 -5
- package/dist/http/agreementFlows.js +18 -24
- package/dist/http/index.d.ts +3 -3
- package/dist/http/index.js +1 -1
- package/dist/http/operatorHeaders.d.ts +7 -1
- package/dist/http/operatorHeaders.js +8 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/shared/apiError.d.ts +22 -1
- package/dist/shared/apiError.js +60 -3
- package/dist/shared/rateLimit.d.ts +12 -22
- package/dist/shared/rateLimit.js +18 -51
- package/dist/types.d.ts +70 -1
- package/dist/types.js +39 -1
- 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
|
|
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:
|
|
120
|
+
count: links.length,
|
|
143
121
|
status,
|
|
144
|
-
links
|
|
122
|
+
links,
|
|
145
123
|
...(hasActive ? { nextSteps: linkIsReachOnly(env) } : {}),
|
|
146
124
|
};
|
|
147
125
|
},
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
142
|
-
*
|
|
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
|
|
145
|
-
const actorIds =
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
return hit.partyId;
|
|
215
|
+
if (facts.pendingPartyIds.includes(id))
|
|
216
|
+
return id;
|
|
151
217
|
}
|
|
152
|
-
|
|
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
|
-
|
|
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".
|
|
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
|
-
|
|
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'])
|
|
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
|
-
|
|
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'])
|
|
374
|
+
return Array.isArray(data?.['agreements'])
|
|
375
|
+
? data['agreements'].map(shapeAgreement)
|
|
376
|
+
: [];
|
|
294
377
|
}
|
|
295
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|