@ziggs-ai/api-client 0.14.3 → 0.16.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/dist/capabilities/agreementVerbs.d.ts +9 -0
- package/dist/capabilities/agreementVerbs.js +29 -3
- package/dist/capabilities/agreements.js +2 -1
- package/dist/capabilities/artifacts.js +16 -6
- package/dist/capabilities/connections.d.ts +1 -1
- package/dist/capabilities/connections.js +82 -59
- package/dist/capabilities/grants.js +8 -2
- package/dist/capabilities/index.d.ts +1 -0
- package/dist/capabilities/index.js +1 -0
- package/dist/capabilities/links.js +12 -4
- package/dist/capabilities/listedFields.d.ts +17 -0
- package/dist/capabilities/listedFields.js +45 -0
- package/dist/capabilities/marketplace.js +7 -2
- package/dist/capabilities/payments.js +7 -2
- package/dist/capabilities/tasks.js +9 -3
- package/dist/http/AgreementClient.d.ts +29 -6
- package/dist/http/AgreementClient.js +2 -0
- package/dist/http/ArtifactsClient.d.ts +5 -16
- package/dist/http/ArtifactsClient.js +8 -5
- package/dist/http/ChatClient.d.ts +2 -6
- package/dist/http/ConnectionsClient.d.ts +15 -4
- package/dist/http/ConnectionsClient.js +53 -39
- package/dist/http/GrantsClient.d.ts +10 -1
- package/dist/http/GrantsClient.js +12 -2
- package/dist/http/IntroductionsClient.d.ts +11 -4
- package/dist/http/MarketplaceClient.d.ts +7 -0
- package/dist/http/MarketplaceClient.js +12 -3
- package/dist/http/OrgsClient.d.ts +0 -14
- package/dist/http/OrgsClient.js +2 -17
- package/dist/http/PaymentsClient.d.ts +47 -5
- package/dist/http/PaymentsClient.js +66 -22
- package/dist/http/TaskClient.js +1 -1
- package/dist/http/agreementFlows.js +9 -3
- package/dist/http/index.d.ts +1 -1
- package/dist/http/index.js +1 -1
- package/dist/http/paymentGrantSelection.d.ts +58 -0
- package/dist/http/paymentGrantSelection.js +121 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/shared/operatorKey.d.ts +47 -0
- package/dist/shared/operatorKey.js +64 -0
- package/dist/types.d.ts +6 -26
- package/dist/types.js +1 -19
- package/dist/utils/appUrls.d.ts +7 -0
- package/dist/utils/appUrls.js +38 -0
- package/package.json +3 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listTasks } from '../http/TaskClient.js';
|
|
2
2
|
import { fullCreds } from './types.js';
|
|
3
|
+
import { LIST_FIELDS_PARAM, parseListFields, pickListedRows } from './listedFields.js';
|
|
3
4
|
/**
|
|
4
5
|
* One list-tasks verb. MCP and the hosted SDK both used to re-declare the
|
|
5
6
|
* same GET /tasks filters; assignedToMe vs assignedTo precedence then
|
|
@@ -11,8 +12,8 @@ export const listTasksCapability = {
|
|
|
11
12
|
names: { sdk: 'task_list', mcp: 'ziggs_task_list' },
|
|
12
13
|
title: 'List tasks',
|
|
13
14
|
descriptions: {
|
|
14
|
-
sdk: 'List tasks reachable by this agent (GET /tasks). Scope follows the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else.',
|
|
15
|
-
mcp: 'List tasks reachable by the acting agent (GET /tasks). Scope is determined by the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else.',
|
|
15
|
+
sdk: 'List tasks reachable by this agent (GET /tasks). Scope follows the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else. Pass fields to keep only those keys on each row.',
|
|
16
|
+
mcp: 'List tasks reachable by the acting agent (GET /tasks). Scope is determined by the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else. Pass fields to keep only those keys on each row.',
|
|
16
17
|
},
|
|
17
18
|
annotation: 'read-only',
|
|
18
19
|
params: {
|
|
@@ -40,6 +41,7 @@ export const listTasksCapability = {
|
|
|
40
41
|
type: 'boolean',
|
|
41
42
|
description: "Shorthand for assignedTo=<this agent's id>. Takes precedence over assignedTo when both are set.",
|
|
42
43
|
},
|
|
44
|
+
fields: LIST_FIELDS_PARAM,
|
|
43
45
|
},
|
|
44
46
|
needsAgentId: true,
|
|
45
47
|
handler: async (args, env) => {
|
|
@@ -48,13 +50,17 @@ export const listTasksCapability = {
|
|
|
48
50
|
? creds.agentId
|
|
49
51
|
: args['assignedTo'];
|
|
50
52
|
const createdBy = args['createdByMe'] ? creds.agentId : undefined;
|
|
51
|
-
|
|
53
|
+
const listed = await listTasks({
|
|
52
54
|
state: args['state'],
|
|
53
55
|
cursor: args['cursor'],
|
|
54
56
|
limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
|
|
55
57
|
assignedTo,
|
|
56
58
|
...(createdBy ? { createdBy } : {}),
|
|
57
59
|
}, creds);
|
|
60
|
+
const fields = parseListFields(args['fields']);
|
|
61
|
+
return fields
|
|
62
|
+
? { ...listed, tasks: pickListedRows(listed.tasks, fields) }
|
|
63
|
+
: listed;
|
|
58
64
|
},
|
|
59
65
|
};
|
|
60
66
|
export const TASK_CAPABILITIES = [listTasksCapability];
|
|
@@ -34,7 +34,17 @@ export interface ProposeTerms {
|
|
|
34
34
|
/** 1:1 proposal to a specific user or agent (`proposedTo` = their id). */
|
|
35
35
|
export interface ProposeDirectInput extends ProposeTerms {
|
|
36
36
|
proposedTo: string;
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The room the terms are discussed in, when there is one.
|
|
39
|
+
*
|
|
40
|
+
* Optional, matching the server, which has always accepted a proposal
|
|
41
|
+
* without one. Delivery does not depend on it: the create-time agreement
|
|
42
|
+
* event targets the party slots, so the counterparty is told either way; a
|
|
43
|
+
* room only gives them somewhere to talk about it. Required here was what
|
|
44
|
+
* forced a bookkeeping agreement between parties with no conversation to
|
|
45
|
+
* invent a chat id in order to exist.
|
|
46
|
+
*/
|
|
47
|
+
chatId?: string;
|
|
38
48
|
}
|
|
39
49
|
/**
|
|
40
50
|
* Open buyer-broadcast. `audience` selects who may see/claim it:
|
|
@@ -205,16 +215,29 @@ export interface CreateLinkBody {
|
|
|
205
215
|
maxClaims?: number;
|
|
206
216
|
}
|
|
207
217
|
/**
|
|
208
|
-
* The route's
|
|
218
|
+
* The route's answer.
|
|
219
|
+
*
|
|
220
|
+
* `status` is constant — it went — and the two null-able fields are per-ARM,
|
|
221
|
+
* chosen by what the caller put in `to` and therefore known to it before the
|
|
222
|
+
* call. `shareUrl` is present exactly for an open invite.
|
|
223
|
+
*
|
|
224
|
+
* `agreementId` names the row for an agent id and for an open invite, and is
|
|
225
|
+
* null for an EMAIL. That split is what keeps the answer from becoming an
|
|
226
|
+
* existence oracle: the knock returns nothing for "no such person", "already
|
|
227
|
+
* linked", "already knocked" and "that is yourself" alike, so an id on some of
|
|
228
|
+
* those and not others would answer the one question the route refuses to
|
|
229
|
+
* answer. An agent id is different — the route resolves it through root
|
|
230
|
+
* authority and refuses a name that answers to nobody, so it already discloses
|
|
231
|
+
* that target's existence.
|
|
209
232
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* user table, so both say the same thing now: it went. `shareUrl` is null
|
|
213
|
-
* exactly when `to` was named, which the caller already knows.
|
|
233
|
+
* Without it a caller could not reference what it had just created, check on it
|
|
234
|
+
* later, or tell a repeat from a first attempt.
|
|
214
235
|
*/
|
|
215
236
|
export interface CreateLinkResult {
|
|
216
237
|
ok: boolean;
|
|
217
238
|
status: 'sent';
|
|
239
|
+
/** The link row, for a named agent id or an open invite. Null for an email. */
|
|
240
|
+
agreementId: string | null;
|
|
218
241
|
shareUrl: string | null;
|
|
219
242
|
}
|
|
220
243
|
/**
|
|
@@ -444,9 +444,11 @@ export async function createLink(body, creds) {
|
|
|
444
444
|
throw new Error('Invalid response: expected { ok, status: "sent", shareUrl } from POST /agreements/links');
|
|
445
445
|
}
|
|
446
446
|
const shareUrl = data['shareUrl'];
|
|
447
|
+
const agreementId = data['agreementId'];
|
|
447
448
|
return {
|
|
448
449
|
ok: data['ok'] === true,
|
|
449
450
|
status: 'sent',
|
|
451
|
+
agreementId: typeof agreementId === 'string' ? agreementId : null,
|
|
450
452
|
shareUrl: typeof shareUrl === 'string' ? shareUrl : null,
|
|
451
453
|
};
|
|
452
454
|
}
|
|
@@ -28,6 +28,8 @@ export interface ListArtifactsResult {
|
|
|
28
28
|
}
|
|
29
29
|
export interface WriteArtifactInput {
|
|
30
30
|
text: string;
|
|
31
|
+
/** Short list name. An upload defaults to filename when omitted. */
|
|
32
|
+
name?: string;
|
|
31
33
|
contentType?: string;
|
|
32
34
|
visibility?: ArtifactVisibility;
|
|
33
35
|
/** When the artifact is associated with a chat. */
|
|
@@ -47,6 +49,8 @@ export interface WriteArtifactInput {
|
|
|
47
49
|
}
|
|
48
50
|
export type ArtifactFileFormat = 'pdf' | 'docx' | 'hwpx' | 'md' | 'txt' | 'html';
|
|
49
51
|
export interface ArtifactUploadUrlInput {
|
|
52
|
+
/** Short list name. Defaults to filename on the server when omitted. */
|
|
53
|
+
name?: string;
|
|
50
54
|
filename: string;
|
|
51
55
|
mime: string;
|
|
52
56
|
/** Exact UTF-8 / file byte length of the object that will be PUT. */
|
|
@@ -82,22 +86,7 @@ export type ArtifactFileView = Record<string, unknown> & {
|
|
|
82
86
|
extractionError?: string | null;
|
|
83
87
|
filename?: string | null;
|
|
84
88
|
};
|
|
85
|
-
|
|
86
|
-
* Replaces `ContextReader`'s artifact reads + `ContextWriter`'s breadcrumb
|
|
87
|
-
* writes. `visibility: 'agent-private'` is the new home for agent thoughts —
|
|
88
|
-
* they persist, are searchable, but are not visible to other chat parties.
|
|
89
|
-
*/
|
|
90
|
-
/**
|
|
91
|
-
* turn a runtime lane id into a scope the backend can accept.
|
|
92
|
-
*
|
|
93
|
-
* A task with no origin chat runs on the lane `agrn-<agreementId>` (see
|
|
94
|
-
* AgentHost.laneSessionIdForTask). That is a routing key, not a chat: no such
|
|
95
|
-
* chat row exists, so passing it as `chatId` made every breadcrumb and recorded
|
|
96
|
-
* thought on an agreement lane come back 403 "not authorized for this scope" —
|
|
97
|
-
* a fictional scope reading like an auth failure. The work is agreement-scoped,
|
|
98
|
-
* so say so; same rule settled for deliverables.
|
|
99
|
-
*/
|
|
100
|
-
export declare const AGREEMENT_LANE_PREFIX = "agrn-";
|
|
89
|
+
export { AGREEMENT_LANE_PREFIX } from '@ziggs-ai/contracts';
|
|
101
90
|
export declare function artifactScopeForSession(sessionId: string): {
|
|
102
91
|
chatId: string;
|
|
103
92
|
} | {
|
|
@@ -33,17 +33,18 @@ function resolveUploadBytes(input) {
|
|
|
33
33
|
* turn a runtime lane id into a scope the backend can accept.
|
|
34
34
|
*
|
|
35
35
|
* A task with no origin chat runs on the lane `agrn-<agreementId>` (see
|
|
36
|
-
*
|
|
36
|
+
* Agent.laneSessionIdForTask). That is a routing key, not a chat: no such
|
|
37
37
|
* chat row exists, so passing it as `chatId` made every breadcrumb and recorded
|
|
38
38
|
* thought on an agreement lane come back 403 "not authorized for this scope" —
|
|
39
39
|
* a fictional scope reading like an auth failure. The work is agreement-scoped,
|
|
40
40
|
* so say so; same rule settled for deliverables.
|
|
41
41
|
*/
|
|
42
|
-
|
|
42
|
+
import { agreementIdFromLane } from '@ziggs-ai/contracts';
|
|
43
|
+
export { AGREEMENT_LANE_PREFIX } from '@ziggs-ai/contracts';
|
|
43
44
|
export function artifactScopeForSession(sessionId) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const agreementId = agreementIdFromLane(sessionId);
|
|
46
|
+
if (agreementId)
|
|
47
|
+
return { agreementId };
|
|
47
48
|
return { chatId: sessionId };
|
|
48
49
|
}
|
|
49
50
|
export class ArtifactsClient {
|
|
@@ -145,6 +146,7 @@ export class ArtifactsClient {
|
|
|
145
146
|
headers: this._headers(),
|
|
146
147
|
body: JSON.stringify({
|
|
147
148
|
text,
|
|
149
|
+
...(input.name?.trim() ? { name: input.name.trim() } : {}),
|
|
148
150
|
contentType: input.contentType ?? 'text',
|
|
149
151
|
visibility: input.visibility ?? 'chat',
|
|
150
152
|
chatId: input.chatId,
|
|
@@ -214,6 +216,7 @@ export class ArtifactsClient {
|
|
|
214
216
|
}
|
|
215
217
|
const parsed = await this._post('/artifacts/upload-url', {
|
|
216
218
|
filename: input.filename.trim(),
|
|
219
|
+
...(input.name?.trim() ? { name: input.name.trim() } : {}),
|
|
217
220
|
mime: input.mime.trim(),
|
|
218
221
|
byteSize: input.byteSize,
|
|
219
222
|
format: input.format,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SendChatMessageResult } from '@ziggs-ai/contracts';
|
|
1
2
|
import { type Creds } from '../types.js';
|
|
2
3
|
/** Shape of GET /chats/mine items — the backend's ChatReadDto. */
|
|
3
4
|
export interface ChatSummary {
|
|
@@ -41,12 +42,7 @@ export interface SendChatMessageInput {
|
|
|
41
42
|
contentType?: string;
|
|
42
43
|
underAgreementId?: string;
|
|
43
44
|
}
|
|
44
|
-
export
|
|
45
|
-
success: boolean;
|
|
46
|
-
message: string;
|
|
47
|
-
messageId: string;
|
|
48
|
-
chatId: string;
|
|
49
|
-
}
|
|
45
|
+
export type { SendChatMessageResult } from '@ziggs-ai/contracts';
|
|
50
46
|
export type ContextTemporal = 'from-now' | 'from-start';
|
|
51
47
|
export interface AddChatMemberInput {
|
|
52
48
|
chatId: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GrantView } from
|
|
1
|
+
import type { GrantView } from "./grants.js";
|
|
2
2
|
export declare function assertNoLeakedConnectionSecret(serialized: string): void;
|
|
3
3
|
/** Thrown by ConnectionsClient with the HTTP status and raw body attached. */
|
|
4
4
|
export interface ConnectionsError extends Error {
|
|
@@ -56,19 +56,30 @@ export declare class ConnectionsClient {
|
|
|
56
56
|
private readonly operatorKey;
|
|
57
57
|
private readonly agentId?;
|
|
58
58
|
private readonly baseUrl;
|
|
59
|
-
|
|
59
|
+
private readonly laneId?;
|
|
60
|
+
/**
|
|
61
|
+
* @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
|
|
62
|
+
* X-Ziggs-Lane. It tells the broker which hire this wake is in, and the
|
|
63
|
+
* broker reads off it WHO the agent is acting for: an agent that serves
|
|
64
|
+
* several hirers holds all of their grants at once and is the holder of
|
|
65
|
+
* every one, so without a lane one hirer's grant is indistinguishable from
|
|
66
|
+
* another's, and a grant one customer gave can be spent while working for
|
|
67
|
+
* somebody else.
|
|
68
|
+
* ArtifactsClient sends the same header for the same reason.
|
|
69
|
+
*/
|
|
70
|
+
constructor(operatorKey: string, agentId?: string, baseUrl?: string, laneId?: string);
|
|
60
71
|
/**
|
|
61
72
|
* Present a ConnectionGrant to the broker and perform a provider action.
|
|
62
73
|
* Returns the provider result — never the raw OAuth token. Requires an
|
|
63
74
|
* impersonated agent (the grant holder).
|
|
64
75
|
*/
|
|
65
|
-
proxy({ connectionId, grantId, action, payload }: ConnectionProxyParams): Promise<unknown>;
|
|
76
|
+
proxy({ connectionId, grantId, action, payload, }: ConnectionProxyParams): Promise<unknown>;
|
|
66
77
|
/**
|
|
67
78
|
* List ConnectionGrants on a connection. When impersonating an agent the
|
|
68
79
|
* server already scopes rows to that holder; the client-side filter is kept
|
|
69
80
|
* as defense-in-depth (same behavior as the former ZiggsConnectClient).
|
|
70
81
|
*/
|
|
71
|
-
listGrants({ connectionId }: {
|
|
82
|
+
listGrants({ connectionId, }: {
|
|
72
83
|
connectionId: string;
|
|
73
84
|
}): Promise<ConnectionGrant[]>;
|
|
74
85
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getBackendUrl } from
|
|
2
|
-
import { throwApiError } from
|
|
3
|
-
import { buildOperatorHeaders } from
|
|
4
|
-
import { GrantsClient } from
|
|
1
|
+
import { getBackendUrl } from "../utils/urlUtils.js";
|
|
2
|
+
import { throwApiError } from "../shared/apiError.js";
|
|
3
|
+
import { buildOperatorHeaders } from "./operatorHeaders.js";
|
|
4
|
+
import { GrantsClient } from "./GrantsClient.js";
|
|
5
5
|
// defense-in-depth mirror of the backend leak-guard
|
|
6
6
|
// (assertProxyResponseDoesNotLeakTokens). The backend strips the *specific*
|
|
7
7
|
// vault token from the response; clients never see that token, so this layer
|
|
@@ -17,7 +17,7 @@ const LEAKED_SECRET_PATTERNS = [
|
|
|
17
17
|
export function assertNoLeakedConnectionSecret(serialized) {
|
|
18
18
|
for (const re of LEAKED_SECRET_PATTERNS) {
|
|
19
19
|
if (re.test(serialized)) {
|
|
20
|
-
throw new Error(
|
|
20
|
+
throw new Error("connection proxy response withheld: it appears to contain a credential (token-leak guard)");
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -34,29 +34,41 @@ export class ConnectionsClient {
|
|
|
34
34
|
operatorKey;
|
|
35
35
|
agentId;
|
|
36
36
|
baseUrl;
|
|
37
|
-
|
|
37
|
+
laneId;
|
|
38
|
+
/**
|
|
39
|
+
* @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
|
|
40
|
+
* X-Ziggs-Lane. It tells the broker which hire this wake is in, and the
|
|
41
|
+
* broker reads off it WHO the agent is acting for: an agent that serves
|
|
42
|
+
* several hirers holds all of their grants at once and is the holder of
|
|
43
|
+
* every one, so without a lane one hirer's grant is indistinguishable from
|
|
44
|
+
* another's, and a grant one customer gave can be spent while working for
|
|
45
|
+
* somebody else.
|
|
46
|
+
* ArtifactsClient sends the same header for the same reason.
|
|
47
|
+
*/
|
|
48
|
+
constructor(operatorKey, agentId, baseUrl, laneId) {
|
|
38
49
|
if (!operatorKey)
|
|
39
|
-
throw new Error(
|
|
50
|
+
throw new Error("ConnectionsClient: operatorKey is required");
|
|
40
51
|
this.operatorKey = operatorKey;
|
|
41
52
|
this.agentId = agentId;
|
|
42
53
|
this.baseUrl = baseUrl || getBackendUrl();
|
|
54
|
+
this.laneId = laneId;
|
|
43
55
|
}
|
|
44
56
|
/**
|
|
45
57
|
* Present a ConnectionGrant to the broker and perform a provider action.
|
|
46
58
|
* Returns the provider result — never the raw OAuth token. Requires an
|
|
47
59
|
* impersonated agent (the grant holder).
|
|
48
60
|
*/
|
|
49
|
-
async proxy({ connectionId, grantId, action, payload }) {
|
|
61
|
+
async proxy({ connectionId, grantId, action, payload, }) {
|
|
50
62
|
if (!connectionId)
|
|
51
|
-
throw new Error(
|
|
63
|
+
throw new Error("proxy: connectionId is required");
|
|
52
64
|
if (!grantId)
|
|
53
|
-
throw new Error(
|
|
65
|
+
throw new Error("proxy: grantId is required");
|
|
54
66
|
if (!action)
|
|
55
|
-
throw new Error(
|
|
67
|
+
throw new Error("proxy: action is required");
|
|
56
68
|
if (!this.agentId) {
|
|
57
|
-
throw new Error(
|
|
69
|
+
throw new Error("proxy: agentId is required — connection broker calls must impersonate the grant holder agent");
|
|
58
70
|
}
|
|
59
|
-
const text = await this._requestRaw(
|
|
71
|
+
const text = await this._requestRaw("POST", `/connections/${encodeURIComponent(connectionId)}/proxy`, { grantId, action, payload: payload ?? {} });
|
|
60
72
|
assertNoLeakedConnectionSecret(text);
|
|
61
73
|
let parsed = text;
|
|
62
74
|
try {
|
|
@@ -65,7 +77,7 @@ export class ConnectionsClient {
|
|
|
65
77
|
catch {
|
|
66
78
|
parsed = text;
|
|
67
79
|
}
|
|
68
|
-
const result = parsed?.[
|
|
80
|
+
const result = parsed?.["result"];
|
|
69
81
|
return result ?? parsed;
|
|
70
82
|
}
|
|
71
83
|
/**
|
|
@@ -73,15 +85,15 @@ export class ConnectionsClient {
|
|
|
73
85
|
* server already scopes rows to that holder; the client-side filter is kept
|
|
74
86
|
* as defense-in-depth (same behavior as the former ZiggsConnectClient).
|
|
75
87
|
*/
|
|
76
|
-
async listGrants({ connectionId }) {
|
|
88
|
+
async listGrants({ connectionId, }) {
|
|
77
89
|
if (!connectionId)
|
|
78
|
-
throw new Error(
|
|
90
|
+
throw new Error("listGrants: connectionId is required");
|
|
79
91
|
if (!this.agentId) {
|
|
80
|
-
throw new Error(
|
|
92
|
+
throw new Error("listGrants: agentId is required — grants are scoped to the impersonated agent");
|
|
81
93
|
}
|
|
82
|
-
const res = (await this._request(
|
|
83
|
-
const grants = res[
|
|
84
|
-
return grants.filter((g) => g[
|
|
94
|
+
const res = (await this._request("GET", `/connections/${encodeURIComponent(connectionId)}/grants`, undefined));
|
|
95
|
+
const grants = res["grants"] || [];
|
|
96
|
+
return grants.filter((g) => g["holderId"] === this.agentId);
|
|
85
97
|
}
|
|
86
98
|
/**
|
|
87
99
|
* cross-connection discovery over the unified GET /grants:
|
|
@@ -91,18 +103,18 @@ export class ConnectionsClient {
|
|
|
91
103
|
* defensively for leaked secrets, as `proxy` does.
|
|
92
104
|
*/
|
|
93
105
|
async listForHolder() {
|
|
94
|
-
const grantsClient = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl);
|
|
106
|
+
const grantsClient = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl, this.laneId);
|
|
95
107
|
// All pages of the agent's live connection grants (not just the first page).
|
|
96
108
|
const { items, unreadableRails } = await grantsClient.listAllGrantsWithRails({
|
|
97
|
-
scopeKind:
|
|
98
|
-
health:
|
|
109
|
+
scopeKind: "connection",
|
|
110
|
+
health: "active",
|
|
99
111
|
});
|
|
100
112
|
// An unreadable rail is not an empty one. Returning [] here made
|
|
101
113
|
// every caller say "no connection grant" when the truth was "this key may not
|
|
102
114
|
// look" — the needs gate refused work the agent could do, and the MCP tool
|
|
103
115
|
// told the owner to issue a grant that already existed. Throwing puts the
|
|
104
116
|
// missing scope in the message, where the person who can fix it will read it.
|
|
105
|
-
const railBlocked = unreadableRails.find((r) => r.rail ===
|
|
117
|
+
const railBlocked = unreadableRails.find((r) => r.rail === "connection");
|
|
106
118
|
if (railBlocked && items.length === 0) {
|
|
107
119
|
throw new Error(`Cannot read this agent's connection grants: the operator key is missing the ` +
|
|
108
120
|
`"${railBlocked.requiredScope}" scope, so the grant list came back empty whether or not ` +
|
|
@@ -125,10 +137,10 @@ export class ConnectionsClient {
|
|
|
125
137
|
/** Issue a connection grant to an agent holder (connection owner side). */
|
|
126
138
|
async issueGrant({ connectionId, holderId, caveats, }) {
|
|
127
139
|
if (!connectionId)
|
|
128
|
-
throw new Error(
|
|
140
|
+
throw new Error("issueGrant: connectionId is required");
|
|
129
141
|
if (!holderId)
|
|
130
|
-
throw new Error(
|
|
131
|
-
return this._request(
|
|
142
|
+
throw new Error("issueGrant: holderId is required");
|
|
143
|
+
return this._request("POST", `/connections/${encodeURIComponent(connectionId)}/grants`, {
|
|
132
144
|
holderId,
|
|
133
145
|
caveats,
|
|
134
146
|
});
|
|
@@ -141,20 +153,20 @@ export class ConnectionsClient {
|
|
|
141
153
|
*/
|
|
142
154
|
async attenuateGrant({ connectionId, grantId, holderId, caveats, }) {
|
|
143
155
|
if (!connectionId)
|
|
144
|
-
throw new Error(
|
|
156
|
+
throw new Error("attenuateGrant: connectionId is required");
|
|
145
157
|
if (!grantId)
|
|
146
|
-
throw new Error(
|
|
158
|
+
throw new Error("attenuateGrant: grantId is required");
|
|
147
159
|
if (!holderId)
|
|
148
|
-
throw new Error(
|
|
149
|
-
return this._request(
|
|
160
|
+
throw new Error("attenuateGrant: holderId is required");
|
|
161
|
+
return this._request("POST", `/connections/${encodeURIComponent(connectionId)}/grants/${encodeURIComponent(grantId)}/attenuate`, { holderId, caveats });
|
|
150
162
|
}
|
|
151
163
|
/** Revoke a single connection grant. */
|
|
152
164
|
async revokeGrant({ connectionId, grantId, }) {
|
|
153
165
|
if (!connectionId)
|
|
154
|
-
throw new Error(
|
|
166
|
+
throw new Error("revokeGrant: connectionId is required");
|
|
155
167
|
if (!grantId)
|
|
156
|
-
throw new Error(
|
|
157
|
-
return this._request(
|
|
168
|
+
throw new Error("revokeGrant: grantId is required");
|
|
169
|
+
return this._request("DELETE", `/connections/${encodeURIComponent(connectionId)}/grants/${encodeURIComponent(grantId)}`, undefined);
|
|
158
170
|
}
|
|
159
171
|
/**
|
|
160
172
|
* agent-initiated MCP connection request: ask the principal to
|
|
@@ -164,21 +176,23 @@ export class ConnectionsClient {
|
|
|
164
176
|
*/
|
|
165
177
|
async requestMcpConnection({ serverUrl, tools, reason, chatId, onBehalfOfUserId, }) {
|
|
166
178
|
if (!serverUrl)
|
|
167
|
-
throw new Error(
|
|
168
|
-
return (await this._request(
|
|
179
|
+
throw new Error("requestMcpConnection: serverUrl is required");
|
|
180
|
+
return (await this._request("POST", "/connections/mcp/requests", { serverUrl, tools, reason, chatId }, onBehalfOfUserId
|
|
181
|
+
? { "X-On-Behalf-Of-User": onBehalfOfUserId }
|
|
182
|
+
: undefined));
|
|
169
183
|
}
|
|
170
184
|
async _requestRaw(method, path, body, extraHeaders) {
|
|
171
185
|
const init = {
|
|
172
186
|
method,
|
|
173
187
|
headers: buildOperatorHeaders(this.operatorKey, this.agentId, {
|
|
174
|
-
...(body !== undefined ? {
|
|
188
|
+
...(body !== undefined ? { "content-type": "application/json" } : {}),
|
|
175
189
|
...extraHeaders,
|
|
176
|
-
}),
|
|
190
|
+
}, this.laneId),
|
|
177
191
|
};
|
|
178
192
|
if (body !== undefined)
|
|
179
193
|
init.body = JSON.stringify(body);
|
|
180
194
|
const response = await fetch(`${this.baseUrl}${path}`, init);
|
|
181
|
-
const text = await response.text().catch(() =>
|
|
195
|
+
const text = await response.text().catch(() => "");
|
|
182
196
|
if (!response.ok) {
|
|
183
197
|
// ApiError (status/body/code); ConnectionsError remains the
|
|
184
198
|
// documented duck type for callers that branch on `.status`.
|
|
@@ -56,7 +56,16 @@ export declare class GrantsClient {
|
|
|
56
56
|
private readonly operatorKey;
|
|
57
57
|
private readonly agentId?;
|
|
58
58
|
private readonly baseUrl;
|
|
59
|
-
|
|
59
|
+
private readonly laneId?;
|
|
60
|
+
/**
|
|
61
|
+
* @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
|
|
62
|
+
* X-Ziggs-Lane. The server reads off it who the agent is acting for, and
|
|
63
|
+
* answers the spendable rails with only the grants this wake may spend —
|
|
64
|
+
* so an agent serving several customers is never handed a list it has to
|
|
65
|
+
* choose between. Without it the list is everything the agent holds, and
|
|
66
|
+
* the grant it picks may then be refused with nothing saying why.
|
|
67
|
+
*/
|
|
68
|
+
constructor(operatorKey: string, agentId?: string, baseUrl?: string, laneId?: string);
|
|
60
69
|
listGrants(query?: ListGrantsQuery): Promise<ListGrantsResult>;
|
|
61
70
|
/**
|
|
62
71
|
* Every grant matching `query`, following the cursor to completion. Use when a
|
|
@@ -12,12 +12,22 @@ export class GrantsClient {
|
|
|
12
12
|
operatorKey;
|
|
13
13
|
agentId;
|
|
14
14
|
baseUrl;
|
|
15
|
-
|
|
15
|
+
laneId;
|
|
16
|
+
/**
|
|
17
|
+
* @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
|
|
18
|
+
* X-Ziggs-Lane. The server reads off it who the agent is acting for, and
|
|
19
|
+
* answers the spendable rails with only the grants this wake may spend —
|
|
20
|
+
* so an agent serving several customers is never handed a list it has to
|
|
21
|
+
* choose between. Without it the list is everything the agent holds, and
|
|
22
|
+
* the grant it picks may then be refused with nothing saying why.
|
|
23
|
+
*/
|
|
24
|
+
constructor(operatorKey, agentId, baseUrl, laneId) {
|
|
16
25
|
if (!operatorKey)
|
|
17
26
|
throw new Error('GrantsClient: operatorKey is required');
|
|
18
27
|
this.operatorKey = operatorKey;
|
|
19
28
|
this.agentId = agentId;
|
|
20
29
|
this.baseUrl = baseUrl || getBackendUrl();
|
|
30
|
+
this.laneId = laneId;
|
|
21
31
|
}
|
|
22
32
|
async listGrants(query = {}) {
|
|
23
33
|
const url = new URL(`${this.baseUrl}/grants`);
|
|
@@ -41,7 +51,7 @@ export class GrantsClient {
|
|
|
41
51
|
if (query.limit != null)
|
|
42
52
|
url.searchParams.set('limit', String(query.limit));
|
|
43
53
|
const res = await fetch(url.toString(), {
|
|
44
|
-
headers: buildOperatorHeaders(this.operatorKey, this.agentId),
|
|
54
|
+
headers: buildOperatorHeaders(this.operatorKey, this.agentId, undefined, this.laneId),
|
|
45
55
|
});
|
|
46
56
|
const body = await res.text().catch(() => '');
|
|
47
57
|
if (!res.ok) {
|
|
@@ -5,15 +5,16 @@ export interface IntroductionFrom {
|
|
|
5
5
|
orgId: string;
|
|
6
6
|
agentId: string | null;
|
|
7
7
|
agentCreatedAt?: string | null;
|
|
8
|
-
claimedLabel
|
|
8
|
+
claimedLabel: {
|
|
9
9
|
text: string;
|
|
10
10
|
selfChosen: boolean;
|
|
11
11
|
verified: boolean;
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
|
-
* Same text as claimedLabel.text. Not identity
|
|
14
|
+
* Same text as claimedLabel.text. Not identity. Remove after 2026-10-07;
|
|
15
|
+
* readers must use claimedLabel.
|
|
15
16
|
*/
|
|
16
|
-
label
|
|
17
|
+
label?: string;
|
|
17
18
|
}
|
|
18
19
|
export interface IntroductionView {
|
|
19
20
|
token: string;
|
|
@@ -41,7 +42,13 @@ export interface IntroductionView {
|
|
|
41
42
|
counterparty?: {
|
|
42
43
|
principal: string;
|
|
43
44
|
agentId: string | null;
|
|
44
|
-
|
|
45
|
+
claimedLabel: {
|
|
46
|
+
text: string;
|
|
47
|
+
selfChosen: boolean;
|
|
48
|
+
verified: boolean;
|
|
49
|
+
};
|
|
50
|
+
/** Same text as claimedLabel.text. Remove after 2026-10-07. */
|
|
51
|
+
label?: string;
|
|
45
52
|
nextStep: string;
|
|
46
53
|
};
|
|
47
54
|
}
|
|
@@ -16,6 +16,13 @@ export interface PublishOfferPayload {
|
|
|
16
16
|
/** Broadcast audience: 'everyone' (default, fully public) or 'org' (members of your active org only). */
|
|
17
17
|
audience?: BroadcastAudience;
|
|
18
18
|
metadata?: Record<string, unknown>;
|
|
19
|
+
/**
|
|
20
|
+
* Replay handle, sent as the `Idempotency-Key` header — the same way
|
|
21
|
+
* `proposeAgreement` sends it. Re-publishing under the same key resolves to
|
|
22
|
+
* the listing already on the board; a NEW key republishes and retires the
|
|
23
|
+
* previous one. It used to ride in the body on this one route.
|
|
24
|
+
*/
|
|
25
|
+
idempotencyKey?: string;
|
|
19
26
|
}
|
|
20
27
|
export declare function publishOffer(payload: PublishOfferPayload, creds: Creds): Promise<Agreement>;
|
|
21
28
|
export interface PullOffersOptions {
|
|
@@ -21,17 +21,26 @@ function assertCreds(creds, op) {
|
|
|
21
21
|
}
|
|
22
22
|
export async function publishOffer(payload, creds) {
|
|
23
23
|
assertCreds(creds, 'marketplace offer publish');
|
|
24
|
+
const { idempotencyKey, ...bodyData } = payload ?? {};
|
|
25
|
+
const headers = buildHeaders(creds);
|
|
26
|
+
if (idempotencyKey)
|
|
27
|
+
headers['Idempotency-Key'] = idempotencyKey;
|
|
24
28
|
const res = await fetch(`${getMarketplaceBaseUrl()}/offers/publish`, {
|
|
25
29
|
method: 'POST',
|
|
26
|
-
headers
|
|
27
|
-
body: JSON.stringify(
|
|
30
|
+
headers,
|
|
31
|
+
body: JSON.stringify(bodyData),
|
|
28
32
|
});
|
|
29
33
|
if (!res.ok) {
|
|
30
34
|
const body = await res.text().catch(() => '');
|
|
31
35
|
throwApiError(res, body, `Marketplace offer publish failed: ${res.status}`);
|
|
32
36
|
}
|
|
33
37
|
const data = await res.json().catch(() => null);
|
|
34
|
-
|
|
38
|
+
// One key name across every agreement-creating route. This used to read
|
|
39
|
+
// `offer`, which was this route's own name for the same thing.
|
|
40
|
+
if (!data?.['agreement']) {
|
|
41
|
+
throw new Error('Invalid response: expected { agreement } from POST /marketplace/offers/publish');
|
|
42
|
+
}
|
|
43
|
+
return shapeAgreement(data['agreement']);
|
|
35
44
|
}
|
|
36
45
|
export async function pullOffers(options, creds) {
|
|
37
46
|
assertCreds(creds, 'marketplace offers pull');
|
|
@@ -27,20 +27,6 @@ export type OrgResolution = {
|
|
|
27
27
|
* match. Ambiguous names return the candidates rather than guessing.
|
|
28
28
|
*/
|
|
29
29
|
export declare function resolveOrgSelector(orgs: MyOrg[], selector: string): OrgResolution;
|
|
30
|
-
/**
|
|
31
|
-
* MCP OAuth auto-provisioned delegate id prefix.
|
|
32
|
-
*
|
|
33
|
-
* The backend stopped classifying delegates by their id and now reads a stamp
|
|
34
|
-
* on the agent row, which is what freed the prefix to drop the vendor name. A
|
|
35
|
-
* client holds credentials, not rows, so this is still a shape test — but it is
|
|
36
|
-
* a shape test against a NAME, and the only thing it decides is which access
|
|
37
|
-
* endpoint to read. It said `delegate--` until the ids were renamed;
|
|
38
|
-
* left alone, every delegate session would have fallen through to the hosted
|
|
39
|
-
* reader and reported the wrong org and connection state.
|
|
40
|
-
*/
|
|
41
|
-
export declare const MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX = "delegate--";
|
|
42
|
-
/** True when `agentId` is an inbound MCP OAuth auto-provisioned delegate. */
|
|
43
|
-
export declare function isMcpOAuthDelegateAgentId(agentId: string): boolean;
|
|
44
30
|
/**
|
|
45
31
|
* runtime acting org from the server (self-hire / agent
|
|
46
32
|
* row): GET /agents/delegate/access. Moved here from ziggs-mcp's inline
|
package/dist/http/OrgsClient.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isMcpOAuthDelegateSession } from '../shared/operatorKey.js';
|
|
1
2
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
2
3
|
import { throwApiError } from '../shared/apiError.js';
|
|
3
4
|
import { buildOperatorHeaders } from './operatorHeaders.js';
|
|
@@ -42,22 +43,6 @@ export function resolveOrgSelector(orgs, selector) {
|
|
|
42
43
|
return { status: 'ambiguous', matches: byName };
|
|
43
44
|
return { status: 'not-found' };
|
|
44
45
|
}
|
|
45
|
-
/**
|
|
46
|
-
* MCP OAuth auto-provisioned delegate id prefix.
|
|
47
|
-
*
|
|
48
|
-
* The backend stopped classifying delegates by their id and now reads a stamp
|
|
49
|
-
* on the agent row, which is what freed the prefix to drop the vendor name. A
|
|
50
|
-
* client holds credentials, not rows, so this is still a shape test — but it is
|
|
51
|
-
* a shape test against a NAME, and the only thing it decides is which access
|
|
52
|
-
* endpoint to read. It said `delegate--` until the ids were renamed;
|
|
53
|
-
* left alone, every delegate session would have fallen through to the hosted
|
|
54
|
-
* reader and reported the wrong org and connection state.
|
|
55
|
-
*/
|
|
56
|
-
export const MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX = 'delegate--';
|
|
57
|
-
/** True when `agentId` is an inbound MCP OAuth auto-provisioned delegate. */
|
|
58
|
-
export function isMcpOAuthDelegateAgentId(agentId) {
|
|
59
|
-
return !!agentId && agentId.startsWith(MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX);
|
|
60
|
-
}
|
|
61
46
|
/**
|
|
62
47
|
* runtime acting org from the server (self-hire / agent
|
|
63
48
|
* row): GET /agents/delegate/access. Moved here from ziggs-mcp's inline
|
|
@@ -125,7 +110,7 @@ export async function fetchHostedAgentAccess(creds, baseUrl) {
|
|
|
125
110
|
* Claude OAuth delegates → self-hire status; everything else → hosted path.
|
|
126
111
|
*/
|
|
127
112
|
export async function fetchSessionAccess(creds, baseUrl) {
|
|
128
|
-
if (
|
|
113
|
+
if (isMcpOAuthDelegateSession(creds)) {
|
|
129
114
|
return fetchDelegateAccess(creds, baseUrl);
|
|
130
115
|
}
|
|
131
116
|
return fetchHostedAgentAccess(creds, baseUrl);
|