@ziggs-ai/api-client 0.1.29 → 0.2.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 +5 -19
- package/dist/ConnectionManager.d.ts +24 -5
- package/dist/ConnectionManager.js +58 -14
- package/dist/http/ArtifactsClient.d.ts +21 -1
- package/dist/http/ArtifactsClient.js +38 -6
- package/dist/http/ConnectionsClient.d.ts +89 -0
- package/dist/http/ConnectionsClient.js +154 -0
- package/dist/http/ContextGrantsClient.d.ts +27 -0
- package/dist/http/ContextGrantsClient.js +25 -0
- package/dist/http/InboxClient.d.ts +35 -1
- package/dist/http/InboxClient.js +25 -2
- package/dist/http/PaymentsClient.d.ts +94 -0
- package/dist/http/PaymentsClient.js +231 -0
- package/dist/http/TaskClient.d.ts +8 -0
- package/dist/http/grantRails.d.ts +20 -0
- package/dist/http/grantRails.js +50 -0
- package/dist/http/index.d.ts +7 -1
- package/dist/http/index.js +3 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -2
- package/package.json +1 -1
- package/dist/websocket/ControlSocket.d.ts +0 -13
- package/dist/websocket/ControlSocket.js +0 -37
- package/dist/websocket/WebSocketClient.d.ts +0 -86
- package/dist/websocket/WebSocketClient.js +0 -268
- package/dist/websocket/index.d.ts +0 -1
- package/dist/websocket/index.js +0 -1
package/dist/http/InboxClient.js
CHANGED
|
@@ -29,8 +29,15 @@ export class InboxClient {
|
|
|
29
29
|
headers['X-Agent-Id'] = this.agentId;
|
|
30
30
|
return headers;
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
const
|
|
32
|
+
inboxUrl(path, opts) {
|
|
33
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
34
|
+
if (opts.waitSeconds != null && opts.waitSeconds > 0) {
|
|
35
|
+
url.searchParams.set('wait', String(opts.waitSeconds));
|
|
36
|
+
}
|
|
37
|
+
return url.toString();
|
|
38
|
+
}
|
|
39
|
+
async getInbox(opts = {}) {
|
|
40
|
+
const res = await fetch(this.inboxUrl('/inbox', opts), {
|
|
34
41
|
headers: this.headers(),
|
|
35
42
|
});
|
|
36
43
|
const body = await res.text().catch(() => '');
|
|
@@ -39,6 +46,22 @@ export class InboxClient {
|
|
|
39
46
|
}
|
|
40
47
|
return JSON.parse(body);
|
|
41
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Operator-level multiplexed read: one poll for every agent this key's
|
|
51
|
+
* owner runs, each slice fenced to its own agent (an agent-scoped key
|
|
52
|
+
* collapses to its one agent). Ack stays per agent — use a per-agent
|
|
53
|
+
* client's `ack()` after acting on that agent's slice.
|
|
54
|
+
*/
|
|
55
|
+
async getOperatorInbox(opts = {}) {
|
|
56
|
+
const res = await fetch(this.inboxUrl('/inbox/operator', opts), {
|
|
57
|
+
headers: this.headers(),
|
|
58
|
+
});
|
|
59
|
+
const body = await res.text().catch(() => '');
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
throw new Error(`InboxClient.getOperatorInbox ${res.status} ${body.slice(0, 200)}`);
|
|
62
|
+
}
|
|
63
|
+
return JSON.parse(body);
|
|
64
|
+
}
|
|
42
65
|
async ack(scopes) {
|
|
43
66
|
if (!scopes.length)
|
|
44
67
|
throw new Error('InboxClient.ack: scopes are required');
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import type { GrantView } from './grants.js';
|
|
3
|
+
/** Thrown by PaymentsClient with the HTTP status and raw body attached. */
|
|
4
|
+
export interface PaymentsError extends Error {
|
|
5
|
+
status?: number;
|
|
6
|
+
body?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface WalletBalance {
|
|
9
|
+
walletId: string | null;
|
|
10
|
+
currency: string;
|
|
11
|
+
balance: number;
|
|
12
|
+
availableBalance: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* ZIG-894 — the one payments client for every surface (agent-sdk, ziggs-mcp,
|
|
16
|
+
* scripts). Consolidates the former agent-sdk private ZiggsPayClient. Wallet
|
|
17
|
+
* ops ride the operator key; agent-impersonated calls send X-Agent-Id and are
|
|
18
|
+
* policy-gated server-side (transfers above threshold return
|
|
19
|
+
* `approval_required`). Wallet-rail grant mutations (issue / attenuate /
|
|
20
|
+
* revoke) live here; grant *listing* is the unified GET /grants (ZIG-648).
|
|
21
|
+
*/
|
|
22
|
+
export declare class PaymentsClient {
|
|
23
|
+
private readonly operatorKey;
|
|
24
|
+
private readonly agentId?;
|
|
25
|
+
private readonly baseUrl;
|
|
26
|
+
constructor(operatorKey: string, agentId?: string, baseUrl?: string);
|
|
27
|
+
balance(): Promise<WalletBalance>;
|
|
28
|
+
resolve({ userId, agentId }?: {
|
|
29
|
+
userId?: string;
|
|
30
|
+
agentId?: string;
|
|
31
|
+
}): Promise<unknown>;
|
|
32
|
+
transfer({ to, amount, idempotencyKey, description, paymentGrantId, }: {
|
|
33
|
+
to: string;
|
|
34
|
+
amount: number;
|
|
35
|
+
idempotencyKey?: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
paymentGrantId?: string;
|
|
38
|
+
}): Promise<unknown>;
|
|
39
|
+
hold({ amount, idempotencyKey, description, }: {
|
|
40
|
+
amount: number;
|
|
41
|
+
idempotencyKey?: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
}): Promise<unknown>;
|
|
44
|
+
release({ holdId, action, toWalletId, idempotencyKey, }: {
|
|
45
|
+
holdId: string;
|
|
46
|
+
action?: string;
|
|
47
|
+
toWalletId?: string;
|
|
48
|
+
idempotencyKey?: string;
|
|
49
|
+
}): Promise<unknown>;
|
|
50
|
+
history(params?: {
|
|
51
|
+
limit?: number;
|
|
52
|
+
offset?: number;
|
|
53
|
+
type?: string;
|
|
54
|
+
}): Promise<unknown>;
|
|
55
|
+
issueGrant({ holderId, caveats }: {
|
|
56
|
+
holderId: string;
|
|
57
|
+
caveats?: unknown;
|
|
58
|
+
}): Promise<unknown>;
|
|
59
|
+
attenuateGrant({ grantId, holderId, caveats, }: {
|
|
60
|
+
grantId: string;
|
|
61
|
+
holderId: string;
|
|
62
|
+
caveats?: unknown;
|
|
63
|
+
}): Promise<unknown>;
|
|
64
|
+
revokeGrant(grantId: string): Promise<unknown>;
|
|
65
|
+
/**
|
|
66
|
+
* Live wallet grants the acting agent holds, via the unified GET /grants
|
|
67
|
+
* (ZIG-648 retired GET /payments/grants), following the cursor to completion.
|
|
68
|
+
*/
|
|
69
|
+
listGrants(): Promise<GrantView[]>;
|
|
70
|
+
createTopUpIntent({ amount, description, currency, }?: {
|
|
71
|
+
amount?: number;
|
|
72
|
+
description?: string;
|
|
73
|
+
currency?: string;
|
|
74
|
+
}): Promise<unknown>;
|
|
75
|
+
confirmMockIntent(intentId: string): Promise<unknown>;
|
|
76
|
+
faucet({ amount, description, idempotencyKey, }?: {
|
|
77
|
+
amount?: number;
|
|
78
|
+
description?: string;
|
|
79
|
+
idempotencyKey?: string;
|
|
80
|
+
}): Promise<unknown>;
|
|
81
|
+
approvals({ status }?: {
|
|
82
|
+
status?: string;
|
|
83
|
+
}): Promise<unknown[]>;
|
|
84
|
+
getApproval(approvalId: string): Promise<unknown | null>;
|
|
85
|
+
decide(approvalId: string, decision: string, note?: string): Promise<unknown>;
|
|
86
|
+
waitForApproval(approvalId: string, opts?: {
|
|
87
|
+
pollMs?: number;
|
|
88
|
+
timeoutMs?: number;
|
|
89
|
+
signal?: AbortSignal;
|
|
90
|
+
}): Promise<unknown>;
|
|
91
|
+
private _request;
|
|
92
|
+
private _get;
|
|
93
|
+
private _post;
|
|
94
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
+
import { buildOperatorHeaders } from './operatorHeaders.js';
|
|
4
|
+
import { GrantsClient } from './GrantsClient.js';
|
|
5
|
+
function randomIdempotencyKey(prefix = 'op') {
|
|
6
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
7
|
+
}
|
|
8
|
+
function parseError(text, fallback) {
|
|
9
|
+
if (!text)
|
|
10
|
+
return fallback;
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(text);
|
|
13
|
+
return parsed['error'] || parsed['message'] || text;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* ZIG-894 — the one payments client for every surface (agent-sdk, ziggs-mcp,
|
|
21
|
+
* scripts). Consolidates the former agent-sdk private ZiggsPayClient. Wallet
|
|
22
|
+
* ops ride the operator key; agent-impersonated calls send X-Agent-Id and are
|
|
23
|
+
* policy-gated server-side (transfers above threshold return
|
|
24
|
+
* `approval_required`). Wallet-rail grant mutations (issue / attenuate /
|
|
25
|
+
* revoke) live here; grant *listing* is the unified GET /grants (ZIG-648).
|
|
26
|
+
*/
|
|
27
|
+
export class PaymentsClient {
|
|
28
|
+
operatorKey;
|
|
29
|
+
agentId;
|
|
30
|
+
baseUrl;
|
|
31
|
+
constructor(operatorKey, agentId, baseUrl) {
|
|
32
|
+
if (!operatorKey)
|
|
33
|
+
throw new Error('PaymentsClient: operatorKey is required');
|
|
34
|
+
this.operatorKey = operatorKey;
|
|
35
|
+
this.agentId = agentId;
|
|
36
|
+
this.baseUrl = baseUrl || getBackendUrl();
|
|
37
|
+
}
|
|
38
|
+
async balance() {
|
|
39
|
+
const w = (await this._get('/payments/wallet'));
|
|
40
|
+
const wallet = w['wallet'];
|
|
41
|
+
return {
|
|
42
|
+
walletId: wallet?.['walletId'] || null,
|
|
43
|
+
currency: wallet?.['currency'] || 'pez',
|
|
44
|
+
balance: w['balance'] ?? 0,
|
|
45
|
+
availableBalance: w['availableBalance'] ?? 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async resolve({ userId, agentId } = {}) {
|
|
49
|
+
if (!userId && !agentId)
|
|
50
|
+
throw new Error('resolve: provide userId or agentId');
|
|
51
|
+
const params = new URLSearchParams();
|
|
52
|
+
if (userId)
|
|
53
|
+
params.set('userId', userId);
|
|
54
|
+
if (agentId)
|
|
55
|
+
params.set('agentId', agentId);
|
|
56
|
+
const res = (await this._get(`/payments/wallets/resolve?${params}`));
|
|
57
|
+
return res['wallet'] || null;
|
|
58
|
+
}
|
|
59
|
+
async transfer({ to, amount, idempotencyKey, description, paymentGrantId, }) {
|
|
60
|
+
if (!to)
|
|
61
|
+
throw new Error('transfer: `to` is required');
|
|
62
|
+
if (!(Number.isInteger(amount) && amount > 0))
|
|
63
|
+
throw new Error('transfer: `amount` must be a positive integer (cents)');
|
|
64
|
+
if (this.agentId && !paymentGrantId)
|
|
65
|
+
throw new Error('transfer: paymentGrantId is required for agent-impersonated transfers. The wallet owner must have issued a payment grant to this agentId.');
|
|
66
|
+
let toWalletId = to;
|
|
67
|
+
if (!to.startsWith('wal_')) {
|
|
68
|
+
const w = (await this.resolve(to.startsWith('agent_') ? { agentId: to } : { userId: to }));
|
|
69
|
+
if (!w?.['walletId'])
|
|
70
|
+
throw new Error(`transfer: could not resolve wallet for "${to}"`);
|
|
71
|
+
toWalletId = w['walletId'];
|
|
72
|
+
}
|
|
73
|
+
const result = (await this._post('/payments/transfer', {
|
|
74
|
+
toWalletId,
|
|
75
|
+
amount,
|
|
76
|
+
idempotencyKey: idempotencyKey || randomIdempotencyKey('xfer'),
|
|
77
|
+
description,
|
|
78
|
+
paymentGrantId,
|
|
79
|
+
}));
|
|
80
|
+
if (result?.['status'] === 'approval_required') {
|
|
81
|
+
const approval = result['approval'] || {};
|
|
82
|
+
return {
|
|
83
|
+
status: 'approval_required',
|
|
84
|
+
approvalId: approval['approvalId'] || null,
|
|
85
|
+
expiresAt: approval['expiresAt'] || null,
|
|
86
|
+
reason: approval['reason'] || 'Amount exceeds auto-approve threshold',
|
|
87
|
+
toWalletId,
|
|
88
|
+
amount,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const tx = result?.['transaction'];
|
|
92
|
+
return { status: 'transferred', transactionId: tx?.['transactionId'] || null, toWalletId, amount };
|
|
93
|
+
}
|
|
94
|
+
async hold({ amount, idempotencyKey, description, }) {
|
|
95
|
+
if (!(Number.isInteger(amount) && amount > 0))
|
|
96
|
+
throw new Error('hold: `amount` must be a positive integer (cents)');
|
|
97
|
+
return this._post('/payments/hold', {
|
|
98
|
+
amount,
|
|
99
|
+
idempotencyKey: idempotencyKey || randomIdempotencyKey('hold'),
|
|
100
|
+
description,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async release({ holdId, action = 'complete', toWalletId, idempotencyKey, }) {
|
|
104
|
+
return this._post(`/payments/release/${holdId}`, {
|
|
105
|
+
idempotencyKey: idempotencyKey || randomIdempotencyKey('rel'),
|
|
106
|
+
action,
|
|
107
|
+
toWalletId,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async history(params = {}) {
|
|
111
|
+
const q = new URLSearchParams();
|
|
112
|
+
if (params.limit != null)
|
|
113
|
+
q.set('limit', String(params.limit));
|
|
114
|
+
if (params.offset != null)
|
|
115
|
+
q.set('offset', String(params.offset));
|
|
116
|
+
if (params.type)
|
|
117
|
+
q.set('type', params.type);
|
|
118
|
+
return this._get('/payments/history' + (q.toString() ? `?${q}` : ''));
|
|
119
|
+
}
|
|
120
|
+
async issueGrant({ holderId, caveats }) {
|
|
121
|
+
return this._post('/payments/grants', { holderId, caveats });
|
|
122
|
+
}
|
|
123
|
+
async attenuateGrant({ grantId, holderId, caveats, }) {
|
|
124
|
+
return this._post(`/payments/grants/${grantId}/attenuate`, { holderId, caveats });
|
|
125
|
+
}
|
|
126
|
+
async revokeGrant(grantId) {
|
|
127
|
+
return this._request('DELETE', `/payments/grants/${grantId}`, undefined);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Live wallet grants the acting agent holds, via the unified GET /grants
|
|
131
|
+
* (ZIG-648 retired GET /payments/grants), following the cursor to completion.
|
|
132
|
+
*/
|
|
133
|
+
async listGrants() {
|
|
134
|
+
const grants = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl);
|
|
135
|
+
return grants.listAllGrants({ scopeKind: 'wallet', health: 'active' });
|
|
136
|
+
}
|
|
137
|
+
async createTopUpIntent({ amount, description, currency, } = {}) {
|
|
138
|
+
return this._post('/payments/onramp/intent', { amount, description, currency });
|
|
139
|
+
}
|
|
140
|
+
async confirmMockIntent(intentId) {
|
|
141
|
+
return this._post(`/payments/onramp/mock-confirm/${intentId}`, {});
|
|
142
|
+
}
|
|
143
|
+
async faucet({ amount, description = 'Faucet', idempotencyKey, } = {}) {
|
|
144
|
+
return this._post('/payments/wallet/fund', {
|
|
145
|
+
amount,
|
|
146
|
+
idempotencyKey: idempotencyKey || randomIdempotencyKey('faucet'),
|
|
147
|
+
description,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async approvals({ status = 'pending' } = {}) {
|
|
151
|
+
const res = (await this._get(`/payments/approvals?status=${encodeURIComponent(status)}`));
|
|
152
|
+
return res['approvals'] || [];
|
|
153
|
+
}
|
|
154
|
+
async getApproval(approvalId) {
|
|
155
|
+
if (!approvalId)
|
|
156
|
+
throw new Error('getApproval: approvalId is required');
|
|
157
|
+
try {
|
|
158
|
+
const res = (await this._get(`/payments/approvals/${approvalId}`));
|
|
159
|
+
return res?.['approval'] || null;
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
if (err.status === 404)
|
|
163
|
+
return null;
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async decide(approvalId, decision, note = '') {
|
|
168
|
+
return this._post(`/payments/approvals/${approvalId}/decide`, { decision, note });
|
|
169
|
+
}
|
|
170
|
+
async waitForApproval(approvalId, opts = {}) {
|
|
171
|
+
const pollMs = Math.max(500, opts.pollMs ?? 3_000);
|
|
172
|
+
const timeoutMs = Math.max(pollMs, opts.timeoutMs ?? 120_000);
|
|
173
|
+
const signal = opts.signal;
|
|
174
|
+
const deadline = Date.now() + timeoutMs;
|
|
175
|
+
const sleep = (ms) => new Promise((resolve, reject) => {
|
|
176
|
+
const timer = setTimeout(resolve, ms);
|
|
177
|
+
if (signal) {
|
|
178
|
+
if (signal.aborted) {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
reject(new Error('aborted'));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
signal.addEventListener('abort', () => {
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
reject(new Error('aborted'));
|
|
186
|
+
}, { once: true });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
while (true) {
|
|
190
|
+
if (signal?.aborted)
|
|
191
|
+
throw new Error('aborted');
|
|
192
|
+
const approval = (await this.getApproval(approvalId));
|
|
193
|
+
if (!approval)
|
|
194
|
+
return { status: 'gone' };
|
|
195
|
+
const s = approval['status'];
|
|
196
|
+
if (s === 'executed')
|
|
197
|
+
return { status: 'executed', approval, transactionId: approval['executedTransactionId'] || null };
|
|
198
|
+
if (s === 'rejected')
|
|
199
|
+
return { status: 'rejected', approval };
|
|
200
|
+
if (s === 'expired')
|
|
201
|
+
return { status: 'expired', approval };
|
|
202
|
+
const remaining = deadline - Date.now();
|
|
203
|
+
if (remaining <= 0)
|
|
204
|
+
return { status: 'timeout', approval };
|
|
205
|
+
await sleep(Math.min(pollMs, remaining));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async _request(method, path, body) {
|
|
209
|
+
const init = {
|
|
210
|
+
method,
|
|
211
|
+
headers: buildOperatorHeaders(this.operatorKey, this.agentId, body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
212
|
+
};
|
|
213
|
+
if (body !== undefined)
|
|
214
|
+
init.body = JSON.stringify(body);
|
|
215
|
+
const response = await fetch(`${this.baseUrl}${path}`, init);
|
|
216
|
+
const text = await response.text();
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
const err = new Error(parseError(text, `HTTP ${response.status}`));
|
|
219
|
+
err.status = response.status;
|
|
220
|
+
err.body = text;
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
return text ? JSON.parse(text) : null;
|
|
224
|
+
}
|
|
225
|
+
_get(path) {
|
|
226
|
+
return this._request('GET', path, undefined);
|
|
227
|
+
}
|
|
228
|
+
_post(path, body) {
|
|
229
|
+
return this._request('POST', path, body ?? {});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -16,6 +16,14 @@ export declare function getTask(taskId: string, creds: Creds): Promise<Task>;
|
|
|
16
16
|
export interface UpdateTaskStateData {
|
|
17
17
|
result?: unknown;
|
|
18
18
|
errorMessage?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Optional idempotency key. A redelivered transition carrying the same key
|
|
21
|
+
* no-ops server-side (returns the current task) instead of erroring on an
|
|
22
|
+
* already-terminal task. Sent in the body — the backend reads
|
|
23
|
+
* `UpdateTaskStateDto.idempotencyKey`. Derive it deterministically so a
|
|
24
|
+
* stateless crash-replay reproduces it.
|
|
25
|
+
*/
|
|
26
|
+
idempotencyKey?: string;
|
|
19
27
|
}
|
|
20
28
|
export declare function updateTaskState(taskId: string, state: TaskState, data: UpdateTaskStateData | undefined, creds: Creds): Promise<Task>;
|
|
21
29
|
export declare function getActiveTasksForAgent(agentId: string, creds: Creds): Promise<Task[]>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GrantScopeKind } from './grants.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read the `scopes` claim of an operator JWT without verifying its signature —
|
|
4
|
+
* an identity hint only, exactly as the backend re-checks the key row on every
|
|
5
|
+
* request. Returns null when the token isn't a decodable JWT (so callers treat
|
|
6
|
+
* entitlement as unknown rather than empty).
|
|
7
|
+
*/
|
|
8
|
+
export declare function decodeOperatorScopes(operatorKey: string): string[] | null;
|
|
9
|
+
export interface UnreadableRail {
|
|
10
|
+
rail: 'context' | 'connection' | 'wallet';
|
|
11
|
+
requiredScope: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Which grant rails the operator key can't read, among those requested (or all
|
|
15
|
+
* rails when no filter is given). Returns null when the key's scopes can't be
|
|
16
|
+
* decoded — the caller then omits the "what I can't see" note rather than
|
|
17
|
+
* guessing. `GET /grants` enforces the same boundary server-side; this only
|
|
18
|
+
* lets the tool surface report it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function unreadableGrantRails(operatorKey: string, requested?: readonly GrantScopeKind[]): UnreadableRail[] | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const RAIL_GROUPS = [
|
|
2
|
+
{ rail: 'context', kinds: ['chat', 'agreement', 'org'], requiredScope: 'context:read' },
|
|
3
|
+
{ rail: 'connection', kinds: ['connection'], requiredScope: 'connections:read' },
|
|
4
|
+
{ rail: 'wallet', kinds: ['wallet'], requiredScope: 'payments:read' },
|
|
5
|
+
];
|
|
6
|
+
/**
|
|
7
|
+
* Read the `scopes` claim of an operator JWT without verifying its signature —
|
|
8
|
+
* an identity hint only, exactly as the backend re-checks the key row on every
|
|
9
|
+
* request. Returns null when the token isn't a decodable JWT (so callers treat
|
|
10
|
+
* entitlement as unknown rather than empty).
|
|
11
|
+
*/
|
|
12
|
+
export function decodeOperatorScopes(operatorKey) {
|
|
13
|
+
const parts = (operatorKey ?? '').trim().split('.');
|
|
14
|
+
if (parts.length !== 3)
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
const json = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
18
|
+
const payload = JSON.parse(json);
|
|
19
|
+
if (!Array.isArray(payload.scopes))
|
|
20
|
+
return null;
|
|
21
|
+
return payload.scopes.filter((s) => typeof s === 'string');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Which grant rails the operator key can't read, among those requested (or all
|
|
29
|
+
* rails when no filter is given). Returns null when the key's scopes can't be
|
|
30
|
+
* decoded — the caller then omits the "what I can't see" note rather than
|
|
31
|
+
* guessing. `GET /grants` enforces the same boundary server-side; this only
|
|
32
|
+
* lets the tool surface report it.
|
|
33
|
+
*/
|
|
34
|
+
export function unreadableGrantRails(operatorKey, requested) {
|
|
35
|
+
const scopes = decodeOperatorScopes(operatorKey);
|
|
36
|
+
if (scopes === null)
|
|
37
|
+
return null;
|
|
38
|
+
const held = new Set(scopes);
|
|
39
|
+
const wanted = requested && requested.length > 0 ? new Set(requested) : null;
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const group of RAIL_GROUPS) {
|
|
42
|
+
const inScope = wanted ? group.kinds.some((k) => wanted.has(k)) : true;
|
|
43
|
+
if (!inScope)
|
|
44
|
+
continue;
|
|
45
|
+
if (!held.has(group.requiredScope)) {
|
|
46
|
+
out.push({ rail: group.rail, requiredScope: group.requiredScope });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
package/dist/http/index.d.ts
CHANGED
|
@@ -13,9 +13,15 @@ export type { DiscoverableItem } from './ContextDiscoveryClient.js';
|
|
|
13
13
|
export { GrantsClient } from './GrantsClient.js';
|
|
14
14
|
export type { ListGrantsQuery, ListGrantsResult } from './GrantsClient.js';
|
|
15
15
|
export { ContextGrantsClient } from './ContextGrantsClient.js';
|
|
16
|
-
export type { ContextGrantRecord, ContextGrantScope, ContextGrantScopeKind, ContextTemporal, IssueContextGrantInput, DelegateContextGrantInput, } from './ContextGrantsClient.js';
|
|
16
|
+
export type { ContextGrantRecord, ContextGrantScope, ContextGrantScopeKind, ContextTemporal, IssueContextGrantInput, DelegateContextGrantInput, DelegateContextGrantResult, ReachEntry, GrantReachResult, } from './ContextGrantsClient.js';
|
|
17
17
|
export { grantCaveat } from './grants.js';
|
|
18
18
|
export type { GrantView, GrantScopeKind, GrantScopeView, GrantCaveatView, GrantHealth, } from './grants.js';
|
|
19
|
+
export { decodeOperatorScopes, unreadableGrantRails } from './grantRails.js';
|
|
20
|
+
export type { UnreadableRail } from './grantRails.js';
|
|
21
|
+
export { PaymentsClient } from './PaymentsClient.js';
|
|
22
|
+
export type { PaymentsError, WalletBalance } from './PaymentsClient.js';
|
|
23
|
+
export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
|
|
24
|
+
export type { ConnectionsError, ConnectionProxyParams, McpConnectionRequestParams, } from './ConnectionsClient.js';
|
|
19
25
|
export { AgentSearchClient } from './AgentSearchClient.js';
|
|
20
26
|
export { TelemetryClient } from './TelemetryClient.js';
|
|
21
27
|
export { InboxClient } from './InboxClient.js';
|
package/dist/http/index.js
CHANGED
|
@@ -9,6 +9,9 @@ export { ContextDiscoveryClient } from './ContextDiscoveryClient.js';
|
|
|
9
9
|
export { GrantsClient } from './GrantsClient.js';
|
|
10
10
|
export { ContextGrantsClient } from './ContextGrantsClient.js';
|
|
11
11
|
export { grantCaveat } from './grants.js';
|
|
12
|
+
export { decodeOperatorScopes, unreadableGrantRails } from './grantRails.js';
|
|
13
|
+
export { PaymentsClient } from './PaymentsClient.js';
|
|
14
|
+
export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
|
|
12
15
|
export { AgentSearchClient } from './AgentSearchClient.js';
|
|
13
16
|
export { TelemetryClient } from './TelemetryClient.js';
|
|
14
17
|
export { InboxClient } from './InboxClient.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export * from './http/index.js';
|
|
2
2
|
export * from './relay/provisionRelayWorkers.js';
|
|
3
|
-
export { WebSocketClient } from './websocket/index.js';
|
|
4
|
-
export { createControlSocket } from './websocket/ControlSocket.js';
|
|
5
3
|
export { ConnectionManager } from './ConnectionManager.js';
|
|
6
4
|
export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, BROADCAST_TARGETS, isBroadcastTarget, AGREEMENT_ENGAGEMENT_KIND, isValidContentType } from './types.js';
|
|
7
5
|
export { getBackendUrl, getWebSocketUrl } from './utils/urlUtils.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export * from './http/index.js';
|
|
2
2
|
export * from './relay/provisionRelayWorkers.js';
|
|
3
|
-
export { WebSocketClient } from './websocket/index.js';
|
|
4
|
-
export { createControlSocket } from './websocket/ControlSocket.js';
|
|
5
3
|
export { ConnectionManager } from './ConnectionManager.js';
|
|
6
4
|
export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, BROADCAST_TARGETS, isBroadcastTarget, AGREEMENT_ENGAGEMENT_KIND, isValidContentType } from './types.js';
|
|
7
5
|
export { getBackendUrl, getWebSocketUrl } from './utils/urlUtils.js';
|
package/package.json
CHANGED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { type Socket } from 'socket.io-client';
|
|
2
|
-
export interface ControlSocketOptions {
|
|
3
|
-
wsUrl: string;
|
|
4
|
-
operatorKey: string;
|
|
5
|
-
agentIds: () => string[];
|
|
6
|
-
onWake: (agentId: string) => void;
|
|
7
|
-
}
|
|
8
|
-
export interface ControlSocketHandle {
|
|
9
|
-
socket: Socket;
|
|
10
|
-
close: () => void;
|
|
11
|
-
resync: () => void;
|
|
12
|
-
}
|
|
13
|
-
export declare function createControlSocket({ wsUrl, operatorKey, agentIds, onWake }: ControlSocketOptions): ControlSocketHandle;
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { io } from 'socket.io-client';
|
|
2
|
-
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
|
-
export function createControlSocket({ wsUrl, operatorKey, agentIds, onWake }) {
|
|
4
|
-
if (!wsUrl || !operatorKey)
|
|
5
|
-
throw new Error('[createControlSocket] wsUrl and operatorKey are required');
|
|
6
|
-
if (typeof agentIds !== 'function')
|
|
7
|
-
throw new Error('[createControlSocket] agentIds must be a function returning string[]');
|
|
8
|
-
if (typeof onWake !== 'function')
|
|
9
|
-
throw new Error('[createControlSocket] onWake must be a function');
|
|
10
|
-
const socket = io(wsUrl, {
|
|
11
|
-
auth: { token: `Bearer ${operatorKey}` },
|
|
12
|
-
query: { token: operatorKey, role: 'launcher' },
|
|
13
|
-
extraHeaders: { Authorization: `Bearer ${operatorKey}` },
|
|
14
|
-
transports: ['websocket'],
|
|
15
|
-
reconnection: true,
|
|
16
|
-
reconnectionDelay: 1000,
|
|
17
|
-
reconnectionDelayMax: 30000,
|
|
18
|
-
randomizationFactor: 0.5,
|
|
19
|
-
});
|
|
20
|
-
socket.on('connect', () => {
|
|
21
|
-
const ids = agentIds();
|
|
22
|
-
runtimeLog.info('ControlSocket', `connected (${socket.id}); registering ${ids.length} agent(s)`);
|
|
23
|
-
socket.emit('launcher:register', { agentIds: ids });
|
|
24
|
-
});
|
|
25
|
-
socket.on('launcher:wake', ({ agentId }) => {
|
|
26
|
-
if (agentId)
|
|
27
|
-
onWake(agentId);
|
|
28
|
-
});
|
|
29
|
-
socket.on('disconnect', (reason) => runtimeLog.debug('ControlSocket', `disconnected: ${reason}`));
|
|
30
|
-
socket.on('connect_error', (err) => runtimeLog.warn('ControlSocket', `connect error: ${err.message}`));
|
|
31
|
-
return {
|
|
32
|
-
socket,
|
|
33
|
-
close: () => socket.disconnect(),
|
|
34
|
-
resync: () => { if (socket.connected)
|
|
35
|
-
socket.emit('launcher:register', { agentIds: agentIds() }); },
|
|
36
|
-
};
|
|
37
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import 'dotenv/config';
|
|
2
|
-
import { type MessageHandler } from '../types.js';
|
|
3
|
-
export interface WebSocketClientOptions {
|
|
4
|
-
wsUrl?: string;
|
|
5
|
-
operatorKey?: string;
|
|
6
|
-
agentId?: string;
|
|
7
|
-
/**
|
|
8
|
-
* Optional user JWT (from `POST /users/login`). When set, the socket
|
|
9
|
-
* authenticates as that user — the gateway populates `socket.userId` and
|
|
10
|
-
* routes messages to them as a human user. Mutually exclusive with
|
|
11
|
-
* `operatorKey` + `agentId`. Used by the eval harness to seed chats as a
|
|
12
|
-
* real user instead of impersonating an agent.
|
|
13
|
-
*/
|
|
14
|
-
userToken?: string;
|
|
15
|
-
label?: string;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Mirrors the backend `ResourceEvent` shape. Emitted on the wire as
|
|
19
|
-
* `resource_changed` when a resource the agent has access to mutates.
|
|
20
|
-
*/
|
|
21
|
-
export interface ResourceEvent {
|
|
22
|
-
kind: 'message' | 'artifact' | 'task-state' | 'agreement';
|
|
23
|
-
ts: string;
|
|
24
|
-
resourceId: string;
|
|
25
|
-
chatId?: string;
|
|
26
|
-
agreementId?: string;
|
|
27
|
-
taskId?: string;
|
|
28
|
-
change?: 'created' | 'updated' | 'state-changed';
|
|
29
|
-
reason?: string;
|
|
30
|
-
/**
|
|
31
|
-
* Principal whose action produced the event, when the backend knows it.
|
|
32
|
-
* Hosts drop events the receiving agent itself caused — its own writes
|
|
33
|
-
* aren't news to it.
|
|
34
|
-
*/
|
|
35
|
-
actorId?: string;
|
|
36
|
-
}
|
|
37
|
-
export type ResourceEventHandler = (event: ResourceEvent) => void | Promise<void>;
|
|
38
|
-
export interface SendOptions {
|
|
39
|
-
entryType?: string;
|
|
40
|
-
content_type?: string;
|
|
41
|
-
contentType?: string;
|
|
42
|
-
taskId?: string;
|
|
43
|
-
/**
|
|
44
|
-
* When true, emits a 'chat:chunk' event instead of 'chat'.
|
|
45
|
-
* Use for streaming partial responses; send a final message
|
|
46
|
-
* (partial: false or omitted) to signal end of stream.
|
|
47
|
-
* Requires backend streaming support.
|
|
48
|
-
*/
|
|
49
|
-
partial?: boolean;
|
|
50
|
-
/** Pre-allocated messageId for streaming continuity — links chunks to the final message. */
|
|
51
|
-
messageId?: string;
|
|
52
|
-
}
|
|
53
|
-
export declare class WebSocketClient {
|
|
54
|
-
private wsUrl;
|
|
55
|
-
private operatorKey;
|
|
56
|
-
private agentId;
|
|
57
|
-
private userToken;
|
|
58
|
-
private label;
|
|
59
|
-
private socket;
|
|
60
|
-
private messageHandler;
|
|
61
|
-
private resourceEventHandler;
|
|
62
|
-
constructor(options?: WebSocketClientOptions);
|
|
63
|
-
setMessageHandler(handler: MessageHandler): void;
|
|
64
|
-
/**
|
|
65
|
-
* Subscribe to `resource_changed` notifications — the unified push channel
|
|
66
|
-
* for non-message resource changes (artifact, task-state, agreement). The
|
|
67
|
-
* agent decides whether to pull the corresponding primitive based on
|
|
68
|
-
* `event.kind` + ids. Replaces cursor-based polling for delta detection.
|
|
69
|
-
*/
|
|
70
|
-
setResourceEventHandler(handler: ResourceEventHandler): void;
|
|
71
|
-
private connectHandlers;
|
|
72
|
-
/**
|
|
73
|
-
* Called on every socket connect, including socket.io reconnects.
|
|
74
|
-
* Used for inbox catch-up (ZIG-454) so missed pushes are recovered from MongoDB.
|
|
75
|
-
*/
|
|
76
|
-
onConnect(handler: () => void | Promise<void>): void;
|
|
77
|
-
private _emitConnect;
|
|
78
|
-
private _connect;
|
|
79
|
-
connectAsync(timeout?: number): Promise<this>;
|
|
80
|
-
disconnect(): void;
|
|
81
|
-
send(chatId: string, receiverId: string, content: string, options?: SendOptions): void;
|
|
82
|
-
isConnected(): boolean;
|
|
83
|
-
handleIncomingMessage(payload: unknown): Promise<void>;
|
|
84
|
-
private buildSocketOptions;
|
|
85
|
-
private generateMessageId;
|
|
86
|
-
}
|