@ziggs-ai/api-client 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/capabilities/artifacts.d.ts +28 -0
- package/dist/capabilities/artifacts.js +200 -17
- package/dist/capabilities/context.js +37 -16
- package/dist/capabilities/grants.d.ts +8 -1
- package/dist/capabilities/grants.js +17 -11
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/capabilities/index.js +1 -1
- package/dist/capabilities/types.d.ts +1 -0
- package/dist/capabilities/types.js +4 -2
- package/dist/http/AgreementClient.d.ts +35 -16
- package/dist/http/AgreementClient.js +87 -33
- package/dist/http/ArtifactsClient.d.ts +38 -1
- package/dist/http/ArtifactsClient.js +70 -10
- package/dist/http/ChatClient.js +4 -14
- package/dist/http/ContextGrantsClient.d.ts +21 -1
- package/dist/http/ContextGrantsClient.js +36 -18
- package/dist/http/ContextReadClient.d.ts +30 -3
- package/dist/http/ContextReadClient.js +58 -1
- package/dist/http/InboxClient.d.ts +32 -3
- package/dist/http/MarketplaceClient.d.ts +0 -1
- package/dist/http/MarketplaceClient.js +4 -10
- package/dist/http/PaymentsClient.js +2 -12
- package/dist/http/TaskClient.d.ts +8 -0
- package/dist/http/TaskClient.js +4 -21
- package/dist/http/grants.d.ts +12 -1
- package/dist/http/grants.js +21 -0
- package/dist/http/index.d.ts +4 -4
- package/dist/http/index.js +2 -2
- package/dist/http/operatorHeaders.d.ts +7 -1
- package/dist/http/operatorHeaders.js +8 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/shared/apiError.d.ts +22 -0
- package/dist/shared/apiError.js +57 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.js +19 -0
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -67,16 +62,12 @@ export interface DelegateAgreementData {
|
|
|
67
62
|
executorId: string;
|
|
68
63
|
chatId: string;
|
|
69
64
|
parentAgreementId: string;
|
|
70
|
-
parentTaskId?: string;
|
|
71
65
|
price?: number;
|
|
72
66
|
lifecycle?: string;
|
|
73
67
|
expiresAt?: string;
|
|
74
68
|
maxExecutions?: number;
|
|
75
69
|
agreementDescription?: string;
|
|
76
70
|
payerId?: string;
|
|
77
|
-
plan?: unknown;
|
|
78
|
-
planReviewTiming?: PlanReviewTiming;
|
|
79
|
-
requireMidWorkPlanAck?: boolean;
|
|
80
71
|
idempotencyKey?: string;
|
|
81
72
|
}
|
|
82
73
|
export declare function delegateAgreement(proposalData: DelegateAgreementData, creds: Creds): Promise<Agreement>;
|
|
@@ -91,7 +82,29 @@ export declare function delegateAgreement(proposalData: DelegateAgreementData, c
|
|
|
91
82
|
export declare function respondToAgreement(agreementId: string, action: 'approve' | 'reject', creds: Creds, opts?: {
|
|
92
83
|
ownerUserId?: string | null;
|
|
93
84
|
agreement?: Agreement | null;
|
|
85
|
+
/** Where to send the human when the decision is theirs (ZIG-1087). */
|
|
86
|
+
appUrl?: string;
|
|
94
87
|
}): Promise<Agreement>;
|
|
88
|
+
/**
|
|
89
|
+
* The approval facts a "who decides this?" question is answered from: the party
|
|
90
|
+
* ids still owing a decision, plus the named responder slot.
|
|
91
|
+
*
|
|
92
|
+
* Split out (ZIG-1087) so the inbox card and the respond call answer that
|
|
93
|
+
* question with ONE rule. The card had no rule at all — it offered the respond
|
|
94
|
+
* tool for every pending proposal, including the ones only the human can
|
|
95
|
+
* answer, and the agent found out by 403.
|
|
96
|
+
*/
|
|
97
|
+
export interface PendingApprovalFacts {
|
|
98
|
+
/** Party ids with a pending entry on the approvals ledger. */
|
|
99
|
+
pendingPartyIds: readonly string[];
|
|
100
|
+
/** The direct responder slot, when the proposal names one. */
|
|
101
|
+
proposedTo?: string | null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Which of `candidateIds` holds the pending approval slot, first match wins —
|
|
105
|
+
* so pass them in preference order (agent id before owner principal).
|
|
106
|
+
*/
|
|
107
|
+
export declare function resolvePendingApprovalPartyId(facts: PendingApprovalFacts, candidateIds: ReadonlyArray<string | null | undefined>): string | null;
|
|
95
108
|
/**
|
|
96
109
|
* ZIG-524 — resolve which approvals.partyId the current operator may submit.
|
|
97
110
|
* Checks pending ledger entries against impersonated agent id and owner principal.
|
|
@@ -116,10 +129,6 @@ export interface CounterAgreementData {
|
|
|
116
129
|
lifecycle?: string;
|
|
117
130
|
maxExecutions?: number;
|
|
118
131
|
description?: string;
|
|
119
|
-
/** Override plan `{ steps: [...] }`; omit to copy from original proposal task */
|
|
120
|
-
plan?: unknown;
|
|
121
|
-
planReviewTiming?: PlanReviewTiming;
|
|
122
|
-
requireMidWorkPlanAck?: boolean;
|
|
123
132
|
}
|
|
124
133
|
export declare function counterAgreement(agreementId: string, counter: CounterAgreementData, creds: Creds): Promise<Agreement>;
|
|
125
134
|
export declare function getAgreementStatus(agreementId: string, creds: Creds): Promise<unknown | null>;
|
|
@@ -185,7 +194,15 @@ export declare function claimAgreement(agreementId: string, creds: Creds): Promi
|
|
|
185
194
|
ok: boolean;
|
|
186
195
|
agreement: Agreement;
|
|
187
196
|
}>;
|
|
188
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Link types a caller may ASK for — the server's `CALLER_LINK_TYPES`, not its
|
|
199
|
+
* full `LINK_TYPES`. `space` is deliberately absent: an agreement space gets
|
|
200
|
+
* exactly one system-minted room, and letting a request name that type would
|
|
201
|
+
* burn the root's single slot on an unrelated chat. Shorter than the server
|
|
202
|
+
* union on purpose, which is why this list carries the reason with it.
|
|
203
|
+
*/
|
|
204
|
+
export declare const CHAT_LINK_TYPES: readonly ["origin", "mention", "delegation", "join"];
|
|
205
|
+
export type ChatLinkType = (typeof CHAT_LINK_TYPES)[number];
|
|
189
206
|
export declare function linkAgreementToChat(agreementId: string, chatId: string, linkType: ChatLinkType | undefined, creds: Creds): Promise<unknown | null>;
|
|
190
207
|
export declare function getChatsForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
|
|
191
208
|
export declare function joinAgreement(agreementId: string, creds: Creds): Promise<{
|
|
@@ -193,10 +210,12 @@ export declare function joinAgreement(agreementId: string, creds: Creds): Promis
|
|
|
193
210
|
agentId: string | null;
|
|
194
211
|
isNew: boolean;
|
|
195
212
|
}>;
|
|
196
|
-
export
|
|
213
|
+
export declare const ARTIFACT_LINK_TYPES: readonly ["produced", "referenced"];
|
|
214
|
+
export type ArtifactLinkType = (typeof ARTIFACT_LINK_TYPES)[number];
|
|
197
215
|
export declare function linkArtifactToAgreement(agreementId: string, artifactId: string, linkType: ArtifactLinkType | undefined, creds: Creds): Promise<unknown | null>;
|
|
198
216
|
export declare function getArtifactsForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
|
|
199
|
-
export
|
|
217
|
+
export declare const AGREEMENT_USER_ROLES: readonly ["payer", "provider", "participant", "observer"];
|
|
218
|
+
export type UserRole = (typeof AGREEMENT_USER_ROLES)[number];
|
|
200
219
|
export declare function linkUserToAgreement(agreementId: string, userId: string, role: UserRole, creds: Creds): Promise<unknown | null>;
|
|
201
220
|
export declare function getUsersForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
|
|
202
221
|
export declare class AgreementClient {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
3
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
4
|
-
import { OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, isBroadcastTarget
|
|
4
|
+
import { OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, isBroadcastTarget } from '../types.js';
|
|
5
|
+
import { throwApiError } from '../shared/apiError.js';
|
|
5
6
|
// Lazy: read at call time so dotenv loaded after this module is imported
|
|
6
7
|
// still takes effect. Baking it at module-load time would freeze the URL
|
|
7
8
|
// before the caller's dotenv.config() runs.
|
|
@@ -13,6 +14,9 @@ function buildHeaders(creds) {
|
|
|
13
14
|
'content-type': 'application/json',
|
|
14
15
|
Authorization: `Bearer ${creds.operatorKey}`,
|
|
15
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 } : {}),
|
|
16
20
|
};
|
|
17
21
|
}
|
|
18
22
|
function assertCreds(creds, op) {
|
|
@@ -21,20 +25,6 @@ function assertCreds(creds, op) {
|
|
|
21
25
|
if (!creds?.agentId)
|
|
22
26
|
throw new Error(`agentId is required for ${op}`);
|
|
23
27
|
}
|
|
24
|
-
function parseErrorMessage(responseBody, defaultMessage) {
|
|
25
|
-
if (!responseBody)
|
|
26
|
-
return defaultMessage;
|
|
27
|
-
try {
|
|
28
|
-
const d = JSON.parse(responseBody);
|
|
29
|
-
return d['details'] || d['error'] || d['message'] || defaultMessage;
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return responseBody || defaultMessage;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function throwApiError(response, responseBody, defaultMessage) {
|
|
36
|
-
throw new ApiError(parseErrorMessage(responseBody, defaultMessage), response.status, responseBody);
|
|
37
|
-
}
|
|
38
28
|
export async function proposeAgreement(proposalData, creds) {
|
|
39
29
|
if (!proposalData)
|
|
40
30
|
throw new Error('Proposal data is required for proposal creation');
|
|
@@ -124,43 +114,78 @@ export async function respondToAgreement(agreementId, action, creds, opts = {})
|
|
|
124
114
|
if (!agreement) {
|
|
125
115
|
throw new Error(`Agreement ${agreementId} not found`);
|
|
126
116
|
}
|
|
117
|
+
const partyId = resolveMyPendingApprovalPartyId(agreement, {
|
|
118
|
+
ownerUserId: opts.ownerUserId,
|
|
119
|
+
agentId: creds.agentId,
|
|
120
|
+
});
|
|
127
121
|
const proposedTo = agreement.parties?.proposedTo;
|
|
128
122
|
if (isBroadcastTarget(proposedTo) || isBroadcastTarget(agreement.parties?.payer)) {
|
|
129
123
|
// ZIG-1021: respond is approve/reject on DIRECT proposals only. An open
|
|
130
124
|
// broadcast (quest, standing offer, link invite) has no per-recipient
|
|
131
|
-
// approval slot —
|
|
132
|
-
// (ZIG-719);
|
|
133
|
-
|
|
125
|
+
// approval slot — a BYSTANDER can neither approve nor reject it
|
|
126
|
+
// (ZIG-719); their move is to claim it, which has its own verb.
|
|
127
|
+
//
|
|
128
|
+
// ZIG-1077: with one exception — the caller who HOLDS a pending approval
|
|
129
|
+
// on the row. A hand-off pins its provider and that provider's consent
|
|
130
|
+
// is a real ledger slot even on an open broadcast (ZIG-1059); this guard
|
|
131
|
+
// used to reject before looking, so the provider could not consent
|
|
132
|
+
// through ANY surface and the hand-off sat unclaimable forever. The
|
|
133
|
+
// backend keeps a consented broadcast OPEN for claims.
|
|
134
|
+
if (!partyId) {
|
|
135
|
+
throw new Error(`Agreement ${agreementId} is an open broadcast and has no personal approval slot — respond cannot ${action} it. Claim it with the claim tool (agreement_claim / ziggs_agreement_claim), or ignore it to pass.`);
|
|
136
|
+
}
|
|
134
137
|
}
|
|
135
|
-
|
|
136
|
-
ownerUserId: opts.ownerUserId,
|
|
137
|
-
agentId: creds.agentId,
|
|
138
|
-
});
|
|
139
|
-
if (!partyId) {
|
|
138
|
+
else if (!partyId) {
|
|
140
139
|
throw new Error(`No pending approval entry for this operator on agreement ${agreementId}`);
|
|
141
140
|
}
|
|
141
|
+
// ZIG-1087 — the slot resolved to the principal, not to us. Every call from
|
|
142
|
+
// this client impersonates `creds.agentId` (X-Agent-Id on every request), and
|
|
143
|
+
// `PUT /approvals/:partyId` takes a decision only from the party itself, so
|
|
144
|
+
// sending this would 403 with "You can only submit your own approval" —
|
|
145
|
+
// a condition, from which the caller cannot tell that no retry will ever
|
|
146
|
+
// work. Withholding consent from delegates is deliberate; being silent about
|
|
147
|
+
// it is not.
|
|
148
|
+
if (partyId && partyId !== creds.agentId) {
|
|
149
|
+
throw new Error(`Agreement ${agreementId} is waiting on ${partyId} — your principal, not you. ` +
|
|
150
|
+
`Consent is the human's to give: a delegate can submit its own approval slot and never its principal's, ` +
|
|
151
|
+
`so there is nothing to retry here and no tool that changes it. ` +
|
|
152
|
+
`Ask your human to approve or reject it${opts.appUrl ? ` at ${opts.appUrl}` : ' in the Ziggs app, under Agreements'}, ` +
|
|
153
|
+
`then read the agreement again to see the outcome.`);
|
|
154
|
+
}
|
|
142
155
|
return approveAgreementAsParty(agreementId, partyId, action === 'approve' ? 'approved' : 'rejected', creds);
|
|
143
156
|
}
|
|
144
157
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
158
|
+
* Which of `candidateIds` holds the pending approval slot, first match wins —
|
|
159
|
+
* so pass them in preference order (agent id before owner principal).
|
|
147
160
|
*/
|
|
148
|
-
export function
|
|
149
|
-
const actorIds =
|
|
150
|
-
const pending = (agreement.approvals ?? []).filter((a) => a.status === 'pending' || a.status === 'PENDING');
|
|
161
|
+
export function resolvePendingApprovalPartyId(facts, candidateIds) {
|
|
162
|
+
const actorIds = candidateIds.filter((id) => typeof id === 'string' && id.length > 0);
|
|
151
163
|
for (const id of actorIds) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return hit.partyId;
|
|
164
|
+
if (facts.pendingPartyIds.includes(id))
|
|
165
|
+
return id;
|
|
155
166
|
}
|
|
156
|
-
|
|
167
|
+
// A row with no pending ledger entries is a legacy one (approvals[] predates
|
|
168
|
+
// ZIG-244), and there the named responder slot IS the decision.
|
|
169
|
+
const proposedTo = facts.proposedTo ?? null;
|
|
157
170
|
if (proposedTo &&
|
|
158
171
|
actorIds.includes(proposedTo) &&
|
|
159
|
-
|
|
172
|
+
facts.pendingPartyIds.length === 0) {
|
|
160
173
|
return proposedTo;
|
|
161
174
|
}
|
|
162
175
|
return null;
|
|
163
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* ZIG-524 — resolve which approvals.partyId the current operator may submit.
|
|
179
|
+
* Checks pending ledger entries against impersonated agent id and owner principal.
|
|
180
|
+
*/
|
|
181
|
+
export function resolveMyPendingApprovalPartyId(agreement, opts) {
|
|
182
|
+
return resolvePendingApprovalPartyId({
|
|
183
|
+
pendingPartyIds: (agreement.approvals ?? [])
|
|
184
|
+
.filter((a) => a.status === 'pending' || a.status === 'PENDING')
|
|
185
|
+
.map((a) => a.partyId),
|
|
186
|
+
proposedTo: agreement.parties?.proposedTo ?? null,
|
|
187
|
+
}, [opts.agentId, opts.ownerUserId]);
|
|
188
|
+
}
|
|
164
189
|
/**
|
|
165
190
|
* Record this party's approval decision on an agreement
|
|
166
191
|
* (`PUT /agreements/:agreementId/approvals/:partyId`).
|
|
@@ -410,6 +435,22 @@ export async function claimAgreement(agreementId, creds) {
|
|
|
410
435
|
}
|
|
411
436
|
return data;
|
|
412
437
|
}
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
// Chat links
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
/**
|
|
442
|
+
* Link types a caller may ASK for — the server's `CALLER_LINK_TYPES`, not its
|
|
443
|
+
* full `LINK_TYPES`. `space` is deliberately absent: an agreement space gets
|
|
444
|
+
* exactly one system-minted room, and letting a request name that type would
|
|
445
|
+
* burn the root's single slot on an unrelated chat. Shorter than the server
|
|
446
|
+
* union on purpose, which is why this list carries the reason with it.
|
|
447
|
+
*/
|
|
448
|
+
export const CHAT_LINK_TYPES = [
|
|
449
|
+
'origin',
|
|
450
|
+
'mention',
|
|
451
|
+
'delegation',
|
|
452
|
+
'join',
|
|
453
|
+
];
|
|
413
454
|
export async function linkAgreementToChat(agreementId, chatId, linkType = 'mention', creds) {
|
|
414
455
|
if (!agreementId || !chatId)
|
|
415
456
|
return null;
|
|
@@ -472,6 +513,10 @@ export async function joinAgreement(agreementId, creds) {
|
|
|
472
513
|
}
|
|
473
514
|
return data;
|
|
474
515
|
}
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
// Artifact links
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
export const ARTIFACT_LINK_TYPES = ['produced', 'referenced'];
|
|
475
520
|
export async function linkArtifactToAgreement(agreementId, artifactId, linkType = 'produced', creds) {
|
|
476
521
|
if (!agreementId || !artifactId)
|
|
477
522
|
return null;
|
|
@@ -516,6 +561,15 @@ export async function getArtifactsForAgreement(agreementId, creds) {
|
|
|
516
561
|
return [];
|
|
517
562
|
}
|
|
518
563
|
}
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
// User links
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
export const AGREEMENT_USER_ROLES = [
|
|
568
|
+
'payer',
|
|
569
|
+
'provider',
|
|
570
|
+
'participant',
|
|
571
|
+
'observer',
|
|
572
|
+
];
|
|
519
573
|
export async function linkUserToAgreement(agreementId, userId, role, creds) {
|
|
520
574
|
if (!agreementId || !userId || !role)
|
|
521
575
|
return null;
|
|
@@ -7,6 +7,13 @@ export interface ListArtifactsOptions {
|
|
|
7
7
|
export interface ListArtifactsQuery {
|
|
8
8
|
chatId?: string;
|
|
9
9
|
agreementId?: string;
|
|
10
|
+
taskId?: string;
|
|
11
|
+
/**
|
|
12
|
+
* ZIG-1037 — `'me'` lists artifacts YOU authored, in any scope or none. The
|
|
13
|
+
* only listing that surfaces a free-standing artifact, since the others read
|
|
14
|
+
* attach tables. Mutually exclusive with the scope filters.
|
|
15
|
+
*/
|
|
16
|
+
authoredBy?: 'me';
|
|
10
17
|
}
|
|
11
18
|
export interface ListArtifactsResult {
|
|
12
19
|
artifacts: unknown[];
|
|
@@ -55,11 +62,15 @@ export declare function artifactScopeForSession(sessionId: string): {
|
|
|
55
62
|
export declare class ArtifactsClient {
|
|
56
63
|
private readonly operatorKey;
|
|
57
64
|
private readonly agentId?;
|
|
65
|
+
private readonly laneId?;
|
|
58
66
|
/**
|
|
59
67
|
* @param operatorKey Agent-scoped or fleet operator key.
|
|
60
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.
|
|
61
72
|
*/
|
|
62
|
-
constructor(operatorKey: string, agentId?: string);
|
|
73
|
+
constructor(operatorKey: string, agentId?: string, laneId?: string);
|
|
63
74
|
list(q: ListArtifactsQuery, opts?: ListArtifactsOptions): Promise<ListArtifactsResult>;
|
|
64
75
|
/**
|
|
65
76
|
* Record an agent thought as an `agent-private` artifact. Replaces the
|
|
@@ -84,6 +95,32 @@ export declare class ArtifactsClient {
|
|
|
84
95
|
writeStrict(input: WriteArtifactInput): Promise<{
|
|
85
96
|
artifactId?: string;
|
|
86
97
|
}>;
|
|
98
|
+
/**
|
|
99
|
+
* ZIG-1037 — attach an existing artifact to a chat or a task.
|
|
100
|
+
*
|
|
101
|
+
* Attaching confers nothing on its own: it places the artifact inside the
|
|
102
|
+
* container, and that container's audience (chat members / task parties) can
|
|
103
|
+
* read it from then on. This is how a free-standing artifact reaches anyone
|
|
104
|
+
* without granting it to one specific agent.
|
|
105
|
+
*
|
|
106
|
+
* The agreement equivalent already existed as POST /agreements/:id/artifacts.
|
|
107
|
+
*/
|
|
108
|
+
attachToChat(artifactId: string, chatId: string): Promise<{
|
|
109
|
+
success: boolean;
|
|
110
|
+
}>;
|
|
111
|
+
attachToTask(artifactId: string, taskId: string, role?: 'input' | 'output'): Promise<{
|
|
112
|
+
success: boolean;
|
|
113
|
+
}>;
|
|
114
|
+
private _attach;
|
|
115
|
+
/**
|
|
116
|
+
* ZIG-1037 — at most one container, and none is fine.
|
|
117
|
+
*
|
|
118
|
+
* This used to demand exactly one, which is what made a deliverable fail at the
|
|
119
|
+
* last step when the model had not worked out which container it was standing
|
|
120
|
+
* in. A write with no scope is now a free-standing artifact; `taskId` alone
|
|
121
|
+
* binds it to a task. Both containers at once is still a mistake worth naming,
|
|
122
|
+
* since only one of them would take effect.
|
|
123
|
+
*/
|
|
87
124
|
private _assertScopeXor;
|
|
88
125
|
private _headers;
|
|
89
126
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
|
+
import { throwApiError } from '../shared/apiError.js';
|
|
3
4
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
4
5
|
import { buildOperatorHeaders } from './operatorHeaders.js';
|
|
5
6
|
/**
|
|
@@ -27,25 +28,35 @@ export function artifactScopeForSession(sessionId) {
|
|
|
27
28
|
export class ArtifactsClient {
|
|
28
29
|
operatorKey;
|
|
29
30
|
agentId;
|
|
31
|
+
laneId;
|
|
30
32
|
/**
|
|
31
33
|
* @param operatorKey Agent-scoped or fleet operator key.
|
|
32
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.
|
|
33
38
|
*/
|
|
34
|
-
constructor(operatorKey, agentId) {
|
|
39
|
+
constructor(operatorKey, agentId, laneId) {
|
|
35
40
|
if (!operatorKey)
|
|
36
41
|
throw new Error('ArtifactsClient: operatorKey is required');
|
|
37
42
|
this.operatorKey = operatorKey;
|
|
38
43
|
this.agentId = agentId;
|
|
44
|
+
this.laneId = laneId;
|
|
39
45
|
}
|
|
40
46
|
async list(q, opts = {}) {
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
const selectors = [q.chatId, q.agreementId, q.taskId, q.authoredBy].filter(Boolean);
|
|
48
|
+
if (selectors.length !== 1) {
|
|
49
|
+
throw new Error('ArtifactsClient.list: pass exactly one of chatId, agreementId, taskId, or authoredBy');
|
|
43
50
|
}
|
|
44
51
|
const url = new URL(`${getBackendUrl()}/artifacts`);
|
|
45
52
|
if (q.chatId)
|
|
46
53
|
url.searchParams.set('chatId', q.chatId);
|
|
47
54
|
if (q.agreementId)
|
|
48
55
|
url.searchParams.set('agreementId', q.agreementId);
|
|
56
|
+
if (q.taskId)
|
|
57
|
+
url.searchParams.set('taskId', q.taskId);
|
|
58
|
+
if (q.authoredBy)
|
|
59
|
+
url.searchParams.set('authoredBy', q.authoredBy);
|
|
49
60
|
if (opts.after)
|
|
50
61
|
url.searchParams.set('after', opts.after);
|
|
51
62
|
if (opts.limit != null)
|
|
@@ -53,7 +64,7 @@ export class ArtifactsClient {
|
|
|
53
64
|
const res = await fetch(url.toString(), { headers: this._headers() });
|
|
54
65
|
if (!res.ok) {
|
|
55
66
|
const body = await res.text().catch(() => '');
|
|
56
|
-
|
|
67
|
+
throwApiError(res, body, `listArtifacts failed: ${res.status} ${res.statusText}`);
|
|
57
68
|
}
|
|
58
69
|
return (await res.json());
|
|
59
70
|
}
|
|
@@ -118,7 +129,7 @@ export class ArtifactsClient {
|
|
|
118
129
|
});
|
|
119
130
|
const body = await res.text().catch(() => '');
|
|
120
131
|
if (!res.ok) {
|
|
121
|
-
|
|
132
|
+
throwApiError(res, body, `recordArtifact failed: ${res.status} ${res.statusText}`);
|
|
122
133
|
}
|
|
123
134
|
try {
|
|
124
135
|
const parsed = body ? JSON.parse(body) : {};
|
|
@@ -128,14 +139,63 @@ export class ArtifactsClient {
|
|
|
128
139
|
return {};
|
|
129
140
|
}
|
|
130
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* ZIG-1037 — attach an existing artifact to a chat or a task.
|
|
144
|
+
*
|
|
145
|
+
* Attaching confers nothing on its own: it places the artifact inside the
|
|
146
|
+
* container, and that container's audience (chat members / task parties) can
|
|
147
|
+
* read it from then on. This is how a free-standing artifact reaches anyone
|
|
148
|
+
* without granting it to one specific agent.
|
|
149
|
+
*
|
|
150
|
+
* The agreement equivalent already existed as POST /agreements/:id/artifacts.
|
|
151
|
+
*/
|
|
152
|
+
async attachToChat(artifactId, chatId) {
|
|
153
|
+
return this._attach(`/chats/${encodeURIComponent(chatId)}/artifacts`, {
|
|
154
|
+
artifactId,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async attachToTask(artifactId, taskId, role = 'output') {
|
|
158
|
+
return this._attach(`/tasks/${encodeURIComponent(taskId)}/artifacts`, {
|
|
159
|
+
artifactId,
|
|
160
|
+
role,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async _attach(path, body) {
|
|
164
|
+
const res = await fetch(`${getBackendUrl()}${path}`, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: this._headers(),
|
|
167
|
+
body: JSON.stringify(body),
|
|
168
|
+
});
|
|
169
|
+
const text = await res.text().catch(() => '');
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
throwApiError(res, text, `attachArtifact failed: ${res.status} ${res.statusText}`);
|
|
172
|
+
}
|
|
173
|
+
return { success: true };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* ZIG-1037 — at most one container, and none is fine.
|
|
177
|
+
*
|
|
178
|
+
* This used to demand exactly one, which is what made a deliverable fail at the
|
|
179
|
+
* last step when the model had not worked out which container it was standing
|
|
180
|
+
* in. A write with no scope is now a free-standing artifact; `taskId` alone
|
|
181
|
+
* binds it to a task. Both containers at once is still a mistake worth naming,
|
|
182
|
+
* since only one of them would take effect.
|
|
183
|
+
*/
|
|
131
184
|
_assertScopeXor(input) {
|
|
132
|
-
if (
|
|
133
|
-
|
|
185
|
+
if (input.chatId && input.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.');
|
|
134
196
|
}
|
|
135
197
|
}
|
|
136
198
|
_headers() {
|
|
137
|
-
return buildOperatorHeaders(this.operatorKey, this.agentId, {
|
|
138
|
-
'content-type': 'application/json',
|
|
139
|
-
});
|
|
199
|
+
return buildOperatorHeaders(this.operatorKey, this.agentId, { 'content-type': 'application/json' }, this.laneId);
|
|
140
200
|
}
|
|
141
201
|
}
|
package/dist/http/ChatClient.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
3
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
4
|
-
import {
|
|
4
|
+
import { throwApiError } from '../shared/apiError.js';
|
|
5
5
|
function buildHeaders(creds) {
|
|
6
6
|
return {
|
|
7
7
|
'content-type': 'application/json',
|
|
8
8
|
Authorization: `Bearer ${creds.operatorKey}`,
|
|
9
9
|
'X-Agent-Id': creds.agentId,
|
|
10
|
+
// ZIG-1092 — the wake's lane, so the backend can fence this call to the
|
|
11
|
+
// engagement it belongs to rather than the agent's whole authority.
|
|
12
|
+
...(creds.laneId ? { 'X-Ziggs-Lane': creds.laneId } : {}),
|
|
10
13
|
};
|
|
11
14
|
}
|
|
12
15
|
function assertCreds(creds, op) {
|
|
@@ -15,19 +18,6 @@ function assertCreds(creds, op) {
|
|
|
15
18
|
if (!creds?.agentId)
|
|
16
19
|
throw new Error(`agentId is required for ${op}`);
|
|
17
20
|
}
|
|
18
|
-
function throwApiError(response, responseBody, defaultMessage) {
|
|
19
|
-
let message = defaultMessage;
|
|
20
|
-
if (responseBody) {
|
|
21
|
-
try {
|
|
22
|
-
const d = JSON.parse(responseBody);
|
|
23
|
-
message = d['details'] || d['error'] || d['message'] || defaultMessage;
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
message = responseBody || defaultMessage;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
throw new ApiError(message, response.status, responseBody);
|
|
30
|
-
}
|
|
31
21
|
export async function openConversation(participantId, creds) {
|
|
32
22
|
if (!participantId)
|
|
33
23
|
throw new Error('participantId is required for openConversation');
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import type { GrantView } from './grants.js';
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* ZIG-1037 added `artifact` — the narrowest context scope: one specific artifact,
|
|
5
|
+
* shared without sharing any chat or agreement it sits in. Still the context
|
|
6
|
+
* rail, not a new grant primitive.
|
|
7
|
+
*/
|
|
8
|
+
export type ContextGrantScopeKind = 'chat' | 'agreement' | 'org' | 'artifact';
|
|
4
9
|
export type ContextTemporal = 'from-now' | 'from-start';
|
|
5
10
|
export interface ContextGrantScope {
|
|
6
11
|
kind: ContextGrantScopeKind;
|
|
@@ -80,6 +85,21 @@ export declare class ContextGrantsClient {
|
|
|
80
85
|
* labels-only, grant-fenced server-side.
|
|
81
86
|
*/
|
|
82
87
|
getReach(grantId: string): Promise<GrantReachResult>;
|
|
88
|
+
/**
|
|
89
|
+
* ZIG-1037 — share an artifact THIS agent authored with another agent.
|
|
90
|
+
*
|
|
91
|
+
* Not a delegation: an agent holds no grant over its own output, so there is
|
|
92
|
+
* no parent to attenuate. Authorship is the authority, held by the agent's
|
|
93
|
+
* owner — which is why the receiver decides the outcome. A receiver already
|
|
94
|
+
* inside the sharer's trust boundary (same owner, same org, or an active link)
|
|
95
|
+
* is granted immediately; anyone else opens a request the owner approves,
|
|
96
|
+
* exactly like `delegateGrant`'s new-party path.
|
|
97
|
+
*/
|
|
98
|
+
shareArtifact(artifactId: string, input: {
|
|
99
|
+
holderId: string;
|
|
100
|
+
expiresAt?: string | null;
|
|
101
|
+
chatId?: string;
|
|
102
|
+
}): Promise<DelegateContextGrantResult>;
|
|
83
103
|
revokeGrant(grantId: string): Promise<{
|
|
84
104
|
status: string;
|
|
85
105
|
revokedCount?: number;
|
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
-
import { ApiError } from '../types.js';
|
|
4
3
|
import { buildOperatorHeaders } from './operatorHeaders.js';
|
|
5
|
-
|
|
6
|
-
let message = defaultMessage;
|
|
7
|
-
if (responseBody) {
|
|
8
|
-
try {
|
|
9
|
-
const d = JSON.parse(responseBody);
|
|
10
|
-
message =
|
|
11
|
-
d['details'] ||
|
|
12
|
-
d['error'] ||
|
|
13
|
-
d['message'] ||
|
|
14
|
-
defaultMessage;
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
message = responseBody || defaultMessage;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
throw new ApiError(message, response.status, responseBody);
|
|
21
|
-
}
|
|
4
|
+
import { throwApiError } from '../shared/apiError.js';
|
|
22
5
|
/**
|
|
23
6
|
* ZIG-411 context grant management — list / issue / delegate / revoke.
|
|
24
7
|
*/
|
|
@@ -113,6 +96,41 @@ export class ContextGrantsClient {
|
|
|
113
96
|
truncatedAgreements: parsed.truncatedAgreements ?? 0,
|
|
114
97
|
};
|
|
115
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* ZIG-1037 — share an artifact THIS agent authored with another agent.
|
|
101
|
+
*
|
|
102
|
+
* Not a delegation: an agent holds no grant over its own output, so there is
|
|
103
|
+
* no parent to attenuate. Authorship is the authority, held by the agent's
|
|
104
|
+
* owner — which is why the receiver decides the outcome. A receiver already
|
|
105
|
+
* inside the sharer's trust boundary (same owner, same org, or an active link)
|
|
106
|
+
* is granted immediately; anyone else opens a request the owner approves,
|
|
107
|
+
* exactly like `delegateGrant`'s new-party path.
|
|
108
|
+
*/
|
|
109
|
+
async shareArtifact(artifactId, input) {
|
|
110
|
+
const res = await fetch(`${this.baseUrl}/context/artifacts/${encodeURIComponent(artifactId)}/share`, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: buildOperatorHeaders(this.operatorKey, this.agentId, {
|
|
113
|
+
'content-type': 'application/json',
|
|
114
|
+
}),
|
|
115
|
+
body: JSON.stringify(input),
|
|
116
|
+
});
|
|
117
|
+
const body = await res.text().catch(() => '');
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
throwApiError(res, body, `shareArtifact failed: ${res.status} ${res.statusText}`);
|
|
120
|
+
}
|
|
121
|
+
const parsed = JSON.parse(body);
|
|
122
|
+
if (parsed.status === 'pending_approval' && parsed.agreementId) {
|
|
123
|
+
return {
|
|
124
|
+
status: 'pending_approval',
|
|
125
|
+
agreementId: parsed.agreementId,
|
|
126
|
+
ownerId: parsed.ownerId,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (!parsed.grant?.grantId) {
|
|
130
|
+
throw new Error('Invalid response: expected { grant } or { status: "pending_approval" } from POST /context/artifacts/:id/share');
|
|
131
|
+
}
|
|
132
|
+
return { status: 'granted', grant: parsed.grant };
|
|
133
|
+
}
|
|
116
134
|
async revokeGrant(grantId) {
|
|
117
135
|
const res = await fetch(`${this.baseUrl}/context/grants/${encodeURIComponent(grantId)}`, {
|
|
118
136
|
method: 'DELETE',
|