@tjamescouch/agentchat 0.21.1 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agentchat.js +1 -1
- package/bin/agentchat.ts +1812 -0
- package/lib/allowlist.js +162 -0
- package/lib/client.js +2 -2
- package/lib/client.ts +877 -0
- package/lib/daemon.ts +662 -0
- package/lib/escrow-hooks.ts +391 -0
- package/lib/identity.ts +412 -0
- package/lib/jitter.ts +59 -0
- package/lib/proposals.ts +612 -0
- package/lib/protocol.js +40 -7
- package/lib/receipts.ts +359 -0
- package/lib/reputation.ts +790 -0
- package/lib/server/handlers/admin.js +94 -0
- package/lib/server/handlers/identity.js +16 -0
- package/lib/server/handlers/message.js +19 -6
- package/lib/server/handlers/presence.js +1 -0
- package/lib/server-directory.js +17 -8
- package/lib/server-directory.ts +232 -0
- package/lib/server.js +115 -10
- package/lib/server.ts +698 -0
- package/lib/supervisor/agent-health.sh +107 -0
- package/lib/supervisor/agent-monitor.sh +123 -0
- package/lib/supervisor/agentctl.sh +19 -3
- package/lib/supervisor/god-backup.sh +126 -0
- package/lib/supervisor/god-watchdog.sh +107 -0
- package/lib/supervisor/killswitch.sh +15 -8
- package/lib/types.ts +433 -0
- package/package.json +7 -3
package/lib/proposals.ts
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentChat Proposals Module
|
|
3
|
+
* Handles structured negotiation between agents
|
|
4
|
+
*
|
|
5
|
+
* Proposals enable agents to make verifiable, signed commitments
|
|
6
|
+
* for work, services, or payments.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { generateProposalId, ProposalStatus } from './protocol.js';
|
|
10
|
+
import type { ProposalStatus as ProposalStatusType } from './types.js';
|
|
11
|
+
|
|
12
|
+
// ============ Types ============
|
|
13
|
+
|
|
14
|
+
export interface ProposalInput {
|
|
15
|
+
id?: string;
|
|
16
|
+
from: string;
|
|
17
|
+
to: string;
|
|
18
|
+
task: string;
|
|
19
|
+
amount?: number | null;
|
|
20
|
+
currency?: string | null;
|
|
21
|
+
payment_code?: string | null;
|
|
22
|
+
terms?: string | null;
|
|
23
|
+
expires?: number | null;
|
|
24
|
+
sig: string;
|
|
25
|
+
elo_stake?: number | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface StoredProposal {
|
|
29
|
+
id: string;
|
|
30
|
+
from: string;
|
|
31
|
+
to: string;
|
|
32
|
+
task: string;
|
|
33
|
+
amount: number | null;
|
|
34
|
+
currency: string | null;
|
|
35
|
+
payment_code: string | null;
|
|
36
|
+
terms: string | null;
|
|
37
|
+
expires: number | null;
|
|
38
|
+
status: string;
|
|
39
|
+
created_at: number;
|
|
40
|
+
updated_at: number;
|
|
41
|
+
sig: string;
|
|
42
|
+
proposer_stake: number | null;
|
|
43
|
+
acceptor_stake: number | null;
|
|
44
|
+
stakes_escrowed: boolean;
|
|
45
|
+
response_sig: string | null;
|
|
46
|
+
response_payment_code: string | null;
|
|
47
|
+
completed_at: number | null;
|
|
48
|
+
completion_proof: string | null;
|
|
49
|
+
dispute_reason: string | null;
|
|
50
|
+
reject_reason?: string | null;
|
|
51
|
+
completion_sig?: string;
|
|
52
|
+
completed_by?: string;
|
|
53
|
+
dispute_sig?: string;
|
|
54
|
+
disputed_by?: string;
|
|
55
|
+
disputed_at?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ProposalResult {
|
|
59
|
+
proposal?: StoredProposal;
|
|
60
|
+
error?: string;
|
|
61
|
+
status?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ListOptions {
|
|
65
|
+
status?: string;
|
|
66
|
+
role?: 'from' | 'to';
|
|
67
|
+
limit?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ProposalStats {
|
|
71
|
+
total: number;
|
|
72
|
+
byStatus: Record<string, number>;
|
|
73
|
+
agents: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface FormattedProposal {
|
|
77
|
+
id: string;
|
|
78
|
+
from: string;
|
|
79
|
+
to: string;
|
|
80
|
+
task: string;
|
|
81
|
+
amount: number | null;
|
|
82
|
+
currency: string | null;
|
|
83
|
+
payment_code: string | null;
|
|
84
|
+
terms: string | null;
|
|
85
|
+
expires: number | null;
|
|
86
|
+
status: string;
|
|
87
|
+
created_at: number;
|
|
88
|
+
sig: string;
|
|
89
|
+
elo_stake: number | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ProposalResponseBase {
|
|
93
|
+
proposal_id: string;
|
|
94
|
+
status: string;
|
|
95
|
+
updated_at: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface AcceptResponse extends ProposalResponseBase {
|
|
99
|
+
from: string;
|
|
100
|
+
to: string;
|
|
101
|
+
payment_code: string | null;
|
|
102
|
+
sig: string | null;
|
|
103
|
+
proposer_stake: number | null;
|
|
104
|
+
acceptor_stake: number | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface RejectResponse extends ProposalResponseBase {
|
|
108
|
+
from: string;
|
|
109
|
+
to: string;
|
|
110
|
+
reason: string | null | undefined;
|
|
111
|
+
sig: string | null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface CompleteResponse extends ProposalResponseBase {
|
|
115
|
+
from: string;
|
|
116
|
+
to: string;
|
|
117
|
+
completed_by: string | undefined;
|
|
118
|
+
completed_at: number | null;
|
|
119
|
+
proof: string | null;
|
|
120
|
+
sig: string | undefined;
|
|
121
|
+
elo_stakes: {
|
|
122
|
+
proposer: number;
|
|
123
|
+
acceptor: number;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface DisputeResponse extends ProposalResponseBase {
|
|
128
|
+
from: string;
|
|
129
|
+
to: string;
|
|
130
|
+
disputed_by: string | undefined;
|
|
131
|
+
disputed_at: number | undefined;
|
|
132
|
+
reason: string | null;
|
|
133
|
+
sig: string | undefined;
|
|
134
|
+
elo_stakes: {
|
|
135
|
+
proposer: number;
|
|
136
|
+
acceptor: number;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type ProposalResponse = AcceptResponse | RejectResponse | CompleteResponse | DisputeResponse | ProposalResponseBase;
|
|
141
|
+
|
|
142
|
+
// ============ ProposalStore Class ============
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* In-memory proposal store
|
|
146
|
+
* In production, this could be backed by persistence
|
|
147
|
+
*/
|
|
148
|
+
export class ProposalStore {
|
|
149
|
+
private proposals: Map<string, StoredProposal>;
|
|
150
|
+
private byAgent: Map<string, Set<string>>;
|
|
151
|
+
private cleanupInterval: ReturnType<typeof setInterval> | null;
|
|
152
|
+
|
|
153
|
+
constructor() {
|
|
154
|
+
// Map of proposal_id -> proposal object
|
|
155
|
+
this.proposals = new Map();
|
|
156
|
+
|
|
157
|
+
// Index by agent for quick lookups
|
|
158
|
+
this.byAgent = new Map(); // agent_id -> Set of proposal_ids
|
|
159
|
+
|
|
160
|
+
// Cleanup expired proposals periodically
|
|
161
|
+
this.cleanupInterval = setInterval(() => this.cleanupExpired(), 60000);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create a new proposal
|
|
166
|
+
*/
|
|
167
|
+
create(proposal: ProposalInput): StoredProposal {
|
|
168
|
+
const id = proposal.id || generateProposalId();
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
|
|
171
|
+
const stored: StoredProposal = {
|
|
172
|
+
id,
|
|
173
|
+
from: proposal.from,
|
|
174
|
+
to: proposal.to,
|
|
175
|
+
task: proposal.task,
|
|
176
|
+
amount: proposal.amount || null,
|
|
177
|
+
currency: proposal.currency || null,
|
|
178
|
+
payment_code: proposal.payment_code || null,
|
|
179
|
+
terms: proposal.terms || null,
|
|
180
|
+
expires: proposal.expires ? now + (proposal.expires * 1000) : null,
|
|
181
|
+
status: ProposalStatus.PENDING,
|
|
182
|
+
created_at: now,
|
|
183
|
+
updated_at: now,
|
|
184
|
+
sig: proposal.sig,
|
|
185
|
+
// ELO staking
|
|
186
|
+
proposer_stake: proposal.elo_stake || null,
|
|
187
|
+
acceptor_stake: null,
|
|
188
|
+
stakes_escrowed: false,
|
|
189
|
+
// Response tracking
|
|
190
|
+
response_sig: null,
|
|
191
|
+
response_payment_code: null,
|
|
192
|
+
completed_at: null,
|
|
193
|
+
completion_proof: null,
|
|
194
|
+
dispute_reason: null
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
this.proposals.set(id, stored);
|
|
198
|
+
|
|
199
|
+
// Index by both agents
|
|
200
|
+
this._indexAgent(proposal.from, id);
|
|
201
|
+
this._indexAgent(proposal.to, id);
|
|
202
|
+
|
|
203
|
+
return stored;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get a proposal by ID
|
|
208
|
+
*/
|
|
209
|
+
get(id: string): StoredProposal | null {
|
|
210
|
+
const proposal = this.proposals.get(id);
|
|
211
|
+
if (!proposal) return null;
|
|
212
|
+
|
|
213
|
+
// Check expiration
|
|
214
|
+
if (proposal.expires && Date.now() > proposal.expires) {
|
|
215
|
+
if (proposal.status === ProposalStatus.PENDING) {
|
|
216
|
+
proposal.status = ProposalStatus.EXPIRED;
|
|
217
|
+
proposal.updated_at = Date.now();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return proposal;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Accept a proposal
|
|
226
|
+
* @param id - Proposal ID
|
|
227
|
+
* @param acceptorId - Agent accepting the proposal
|
|
228
|
+
* @param sig - Signature of acceptance
|
|
229
|
+
* @param payment_code - Optional payment code
|
|
230
|
+
* @param acceptor_stake - Optional ELO stake from acceptor
|
|
231
|
+
*/
|
|
232
|
+
accept(
|
|
233
|
+
id: string,
|
|
234
|
+
acceptorId: string,
|
|
235
|
+
sig: string,
|
|
236
|
+
payment_code: string | null = null,
|
|
237
|
+
acceptor_stake: number | null = null
|
|
238
|
+
): ProposalResult {
|
|
239
|
+
const proposal = this.get(id);
|
|
240
|
+
if (!proposal) {
|
|
241
|
+
return { error: 'PROPOSAL_NOT_FOUND' };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (proposal.status !== ProposalStatus.PENDING) {
|
|
245
|
+
return { error: 'PROPOSAL_NOT_PENDING', status: proposal.status };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (proposal.to !== acceptorId) {
|
|
249
|
+
return { error: 'NOT_PROPOSAL_RECIPIENT' };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (proposal.expires && Date.now() > proposal.expires) {
|
|
253
|
+
proposal.status = ProposalStatus.EXPIRED;
|
|
254
|
+
proposal.updated_at = Date.now();
|
|
255
|
+
return { error: 'PROPOSAL_EXPIRED' };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
proposal.status = ProposalStatus.ACCEPTED;
|
|
259
|
+
proposal.response_sig = sig;
|
|
260
|
+
proposal.response_payment_code = payment_code;
|
|
261
|
+
proposal.acceptor_stake = acceptor_stake;
|
|
262
|
+
proposal.updated_at = Date.now();
|
|
263
|
+
|
|
264
|
+
return { proposal };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Reject a proposal
|
|
269
|
+
*/
|
|
270
|
+
reject(
|
|
271
|
+
id: string,
|
|
272
|
+
rejectorId: string,
|
|
273
|
+
sig: string,
|
|
274
|
+
reason: string | null = null
|
|
275
|
+
): ProposalResult {
|
|
276
|
+
const proposal = this.get(id);
|
|
277
|
+
if (!proposal) {
|
|
278
|
+
return { error: 'PROPOSAL_NOT_FOUND' };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (proposal.status !== ProposalStatus.PENDING) {
|
|
282
|
+
return { error: 'PROPOSAL_NOT_PENDING', status: proposal.status };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (proposal.to !== rejectorId) {
|
|
286
|
+
return { error: 'NOT_PROPOSAL_RECIPIENT' };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
proposal.status = ProposalStatus.REJECTED;
|
|
290
|
+
proposal.response_sig = sig;
|
|
291
|
+
proposal.reject_reason = reason;
|
|
292
|
+
proposal.updated_at = Date.now();
|
|
293
|
+
|
|
294
|
+
return { proposal };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Mark a proposal as complete
|
|
299
|
+
*/
|
|
300
|
+
complete(
|
|
301
|
+
id: string,
|
|
302
|
+
completerId: string,
|
|
303
|
+
sig: string,
|
|
304
|
+
proof: string | null = null
|
|
305
|
+
): ProposalResult {
|
|
306
|
+
const proposal = this.get(id);
|
|
307
|
+
if (!proposal) {
|
|
308
|
+
return { error: 'PROPOSAL_NOT_FOUND' };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (proposal.status !== ProposalStatus.ACCEPTED) {
|
|
312
|
+
return { error: 'PROPOSAL_NOT_ACCEPTED', status: proposal.status };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Either party can mark as complete
|
|
316
|
+
if (proposal.from !== completerId && proposal.to !== completerId) {
|
|
317
|
+
return { error: 'NOT_PROPOSAL_PARTY' };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
proposal.status = ProposalStatus.COMPLETED;
|
|
321
|
+
proposal.completed_at = Date.now();
|
|
322
|
+
proposal.completion_proof = proof;
|
|
323
|
+
proposal.completion_sig = sig;
|
|
324
|
+
proposal.completed_by = completerId;
|
|
325
|
+
proposal.updated_at = Date.now();
|
|
326
|
+
|
|
327
|
+
return { proposal };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Dispute a proposal
|
|
332
|
+
*/
|
|
333
|
+
dispute(
|
|
334
|
+
id: string,
|
|
335
|
+
disputerId: string,
|
|
336
|
+
sig: string,
|
|
337
|
+
reason: string
|
|
338
|
+
): ProposalResult {
|
|
339
|
+
const proposal = this.get(id);
|
|
340
|
+
if (!proposal) {
|
|
341
|
+
return { error: 'PROPOSAL_NOT_FOUND' };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Can only dispute accepted proposals
|
|
345
|
+
if (proposal.status !== ProposalStatus.ACCEPTED) {
|
|
346
|
+
return { error: 'PROPOSAL_NOT_ACCEPTED', status: proposal.status };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Either party can dispute
|
|
350
|
+
if (proposal.from !== disputerId && proposal.to !== disputerId) {
|
|
351
|
+
return { error: 'NOT_PROPOSAL_PARTY' };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
proposal.status = ProposalStatus.DISPUTED;
|
|
355
|
+
proposal.dispute_reason = reason;
|
|
356
|
+
proposal.dispute_sig = sig;
|
|
357
|
+
proposal.disputed_by = disputerId;
|
|
358
|
+
proposal.disputed_at = Date.now();
|
|
359
|
+
proposal.updated_at = Date.now();
|
|
360
|
+
|
|
361
|
+
return { proposal };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* List proposals for an agent
|
|
366
|
+
*/
|
|
367
|
+
listByAgent(agentId: string, options: ListOptions = {}): StoredProposal[] {
|
|
368
|
+
const ids = this.byAgent.get(agentId) || new Set<string>();
|
|
369
|
+
let proposals = Array.from(ids)
|
|
370
|
+
.map(id => this.get(id))
|
|
371
|
+
.filter((p): p is StoredProposal => p !== null);
|
|
372
|
+
|
|
373
|
+
// Filter by status
|
|
374
|
+
if (options.status) {
|
|
375
|
+
proposals = proposals.filter(p => p.status === options.status);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Filter by role (from/to)
|
|
379
|
+
if (options.role === 'from') {
|
|
380
|
+
proposals = proposals.filter(p => p.from === agentId);
|
|
381
|
+
} else if (options.role === 'to') {
|
|
382
|
+
proposals = proposals.filter(p => p.to === agentId);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Sort by created_at descending
|
|
386
|
+
proposals.sort((a, b) => b.created_at - a.created_at);
|
|
387
|
+
|
|
388
|
+
// Limit
|
|
389
|
+
if (options.limit) {
|
|
390
|
+
proposals = proposals.slice(0, options.limit);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return proposals;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Index a proposal by agent
|
|
398
|
+
*/
|
|
399
|
+
private _indexAgent(agentId: string, proposalId: string): void {
|
|
400
|
+
if (!this.byAgent.has(agentId)) {
|
|
401
|
+
this.byAgent.set(agentId, new Set());
|
|
402
|
+
}
|
|
403
|
+
this.byAgent.get(agentId)!.add(proposalId);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Clean up expired proposals (older than 24 hours after expiration)
|
|
408
|
+
*/
|
|
409
|
+
cleanupExpired(): void {
|
|
410
|
+
const cutoff = Date.now() - (24 * 60 * 60 * 1000);
|
|
411
|
+
|
|
412
|
+
for (const [id, proposal] of this.proposals) {
|
|
413
|
+
if (proposal.expires && proposal.expires < cutoff) {
|
|
414
|
+
this.proposals.delete(id);
|
|
415
|
+
|
|
416
|
+
// Remove from agent indices
|
|
417
|
+
const fromSet = this.byAgent.get(proposal.from);
|
|
418
|
+
if (fromSet) fromSet.delete(id);
|
|
419
|
+
|
|
420
|
+
const toSet = this.byAgent.get(proposal.to);
|
|
421
|
+
if (toSet) toSet.delete(id);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Stop the cleanup interval
|
|
428
|
+
*/
|
|
429
|
+
close(): void {
|
|
430
|
+
if (this.cleanupInterval) {
|
|
431
|
+
clearInterval(this.cleanupInterval);
|
|
432
|
+
this.cleanupInterval = null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get stats about the proposal store
|
|
438
|
+
*/
|
|
439
|
+
stats(): ProposalStats {
|
|
440
|
+
const byStatus: Record<string, number> = {};
|
|
441
|
+
for (const proposal of this.proposals.values()) {
|
|
442
|
+
byStatus[proposal.status] = (byStatus[proposal.status] || 0) + 1;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
total: this.proposals.size,
|
|
447
|
+
byStatus,
|
|
448
|
+
agents: this.byAgent.size
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ============ Helper Functions ============
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Format a proposal for display/transmission
|
|
457
|
+
*/
|
|
458
|
+
export function formatProposal(proposal: StoredProposal): FormattedProposal {
|
|
459
|
+
return {
|
|
460
|
+
id: proposal.id,
|
|
461
|
+
from: proposal.from,
|
|
462
|
+
to: proposal.to,
|
|
463
|
+
task: proposal.task,
|
|
464
|
+
amount: proposal.amount,
|
|
465
|
+
currency: proposal.currency,
|
|
466
|
+
payment_code: proposal.payment_code,
|
|
467
|
+
terms: proposal.terms,
|
|
468
|
+
expires: proposal.expires,
|
|
469
|
+
status: proposal.status,
|
|
470
|
+
created_at: proposal.created_at,
|
|
471
|
+
sig: proposal.sig,
|
|
472
|
+
elo_stake: proposal.proposer_stake
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Format a proposal response (accept/reject/complete/dispute)
|
|
478
|
+
*/
|
|
479
|
+
export function formatProposalResponse(
|
|
480
|
+
proposal: StoredProposal,
|
|
481
|
+
responseType: 'accept' | 'reject' | 'complete' | 'dispute' | string
|
|
482
|
+
): ProposalResponse {
|
|
483
|
+
const base: ProposalResponseBase = {
|
|
484
|
+
proposal_id: proposal.id,
|
|
485
|
+
status: proposal.status,
|
|
486
|
+
updated_at: proposal.updated_at
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
switch (responseType) {
|
|
490
|
+
case 'accept':
|
|
491
|
+
return {
|
|
492
|
+
...base,
|
|
493
|
+
from: proposal.from,
|
|
494
|
+
to: proposal.to,
|
|
495
|
+
payment_code: proposal.response_payment_code,
|
|
496
|
+
sig: proposal.response_sig,
|
|
497
|
+
proposer_stake: proposal.proposer_stake,
|
|
498
|
+
acceptor_stake: proposal.acceptor_stake
|
|
499
|
+
} as AcceptResponse;
|
|
500
|
+
|
|
501
|
+
case 'reject':
|
|
502
|
+
return {
|
|
503
|
+
...base,
|
|
504
|
+
from: proposal.from,
|
|
505
|
+
to: proposal.to,
|
|
506
|
+
reason: proposal.reject_reason,
|
|
507
|
+
sig: proposal.response_sig
|
|
508
|
+
} as RejectResponse;
|
|
509
|
+
|
|
510
|
+
case 'complete':
|
|
511
|
+
return {
|
|
512
|
+
...base,
|
|
513
|
+
from: proposal.from,
|
|
514
|
+
to: proposal.to,
|
|
515
|
+
completed_by: proposal.completed_by,
|
|
516
|
+
completed_at: proposal.completed_at,
|
|
517
|
+
proof: proposal.completion_proof,
|
|
518
|
+
sig: proposal.completion_sig,
|
|
519
|
+
elo_stakes: {
|
|
520
|
+
proposer: proposal.proposer_stake || 0,
|
|
521
|
+
acceptor: proposal.acceptor_stake || 0
|
|
522
|
+
}
|
|
523
|
+
} as CompleteResponse;
|
|
524
|
+
|
|
525
|
+
case 'dispute':
|
|
526
|
+
return {
|
|
527
|
+
...base,
|
|
528
|
+
from: proposal.from,
|
|
529
|
+
to: proposal.to,
|
|
530
|
+
disputed_by: proposal.disputed_by,
|
|
531
|
+
disputed_at: proposal.disputed_at,
|
|
532
|
+
reason: proposal.dispute_reason,
|
|
533
|
+
sig: proposal.dispute_sig,
|
|
534
|
+
elo_stakes: {
|
|
535
|
+
proposer: proposal.proposer_stake || 0,
|
|
536
|
+
acceptor: proposal.acceptor_stake || 0
|
|
537
|
+
}
|
|
538
|
+
} as DisputeResponse;
|
|
539
|
+
|
|
540
|
+
default:
|
|
541
|
+
return base;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Create proposal content string for signing
|
|
547
|
+
* This ensures both parties sign the same canonical data
|
|
548
|
+
*/
|
|
549
|
+
export function getProposalSigningContent(proposal: {
|
|
550
|
+
to: string;
|
|
551
|
+
task: string;
|
|
552
|
+
amount?: number | null;
|
|
553
|
+
currency?: string | null;
|
|
554
|
+
payment_code?: string | null;
|
|
555
|
+
expires?: number | null;
|
|
556
|
+
elo_stake?: number | null;
|
|
557
|
+
}): string {
|
|
558
|
+
const fields = [
|
|
559
|
+
proposal.to,
|
|
560
|
+
proposal.task,
|
|
561
|
+
proposal.amount || '',
|
|
562
|
+
proposal.currency || '',
|
|
563
|
+
proposal.payment_code || '',
|
|
564
|
+
proposal.expires || '',
|
|
565
|
+
proposal.elo_stake || ''
|
|
566
|
+
];
|
|
567
|
+
return fields.join('|');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Create accept content string for signing
|
|
572
|
+
* @param proposalId - The proposal being accepted
|
|
573
|
+
* @param payment_code - Optional payment code
|
|
574
|
+
* @param elo_stake - Optional ELO stake from acceptor
|
|
575
|
+
*/
|
|
576
|
+
export function getAcceptSigningContent(
|
|
577
|
+
proposalId: string,
|
|
578
|
+
payment_code: string = '',
|
|
579
|
+
elo_stake: string | number = ''
|
|
580
|
+
): string {
|
|
581
|
+
return `ACCEPT|${proposalId}|${payment_code}|${elo_stake}`;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Create reject content string for signing
|
|
586
|
+
*/
|
|
587
|
+
export function getRejectSigningContent(
|
|
588
|
+
proposalId: string,
|
|
589
|
+
reason: string = ''
|
|
590
|
+
): string {
|
|
591
|
+
return `REJECT|${proposalId}|${reason}`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Create complete content string for signing
|
|
596
|
+
*/
|
|
597
|
+
export function getCompleteSigningContent(
|
|
598
|
+
proposalId: string,
|
|
599
|
+
proof: string = ''
|
|
600
|
+
): string {
|
|
601
|
+
return `COMPLETE|${proposalId}|${proof}`;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Create dispute content string for signing
|
|
606
|
+
*/
|
|
607
|
+
export function getDisputeSigningContent(
|
|
608
|
+
proposalId: string,
|
|
609
|
+
reason: string
|
|
610
|
+
): string {
|
|
611
|
+
return `DISPUTE|${proposalId}|${reason}`;
|
|
612
|
+
}
|
package/lib/protocol.js
CHANGED
|
@@ -29,7 +29,11 @@ export const ClientMessageType = {
|
|
|
29
29
|
SET_PRESENCE: 'SET_PRESENCE',
|
|
30
30
|
// Identity verification message types
|
|
31
31
|
VERIFY_REQUEST: 'VERIFY_REQUEST',
|
|
32
|
-
VERIFY_RESPONSE: 'VERIFY_RESPONSE'
|
|
32
|
+
VERIFY_RESPONSE: 'VERIFY_RESPONSE',
|
|
33
|
+
// Admin message types
|
|
34
|
+
ADMIN_APPROVE: 'ADMIN_APPROVE',
|
|
35
|
+
ADMIN_REVOKE: 'ADMIN_REVOKE',
|
|
36
|
+
ADMIN_LIST: 'ADMIN_LIST',
|
|
33
37
|
};
|
|
34
38
|
|
|
35
39
|
// Server -> Client message types
|
|
@@ -59,7 +63,9 @@ export const ServerMessageType = {
|
|
|
59
63
|
VERIFY_REQUEST: 'VERIFY_REQUEST',
|
|
60
64
|
VERIFY_RESPONSE: 'VERIFY_RESPONSE',
|
|
61
65
|
VERIFY_SUCCESS: 'VERIFY_SUCCESS',
|
|
62
|
-
VERIFY_FAILED: 'VERIFY_FAILED'
|
|
66
|
+
VERIFY_FAILED: 'VERIFY_FAILED',
|
|
67
|
+
// Admin response
|
|
68
|
+
ADMIN_RESULT: 'ADMIN_RESULT',
|
|
63
69
|
};
|
|
64
70
|
|
|
65
71
|
// Error codes
|
|
@@ -84,7 +90,9 @@ export const ErrorCode = {
|
|
|
84
90
|
// Verification errors
|
|
85
91
|
VERIFICATION_FAILED: 'VERIFICATION_FAILED',
|
|
86
92
|
VERIFICATION_EXPIRED: 'VERIFICATION_EXPIRED',
|
|
87
|
-
NO_PUBKEY: 'NO_PUBKEY'
|
|
93
|
+
NO_PUBKEY: 'NO_PUBKEY',
|
|
94
|
+
// Allowlist errors
|
|
95
|
+
NOT_ALLOWED: 'NOT_ALLOWED',
|
|
88
96
|
};
|
|
89
97
|
|
|
90
98
|
// Presence status
|
|
@@ -92,7 +100,8 @@ export const PresenceStatus = {
|
|
|
92
100
|
ONLINE: 'online',
|
|
93
101
|
AWAY: 'away',
|
|
94
102
|
BUSY: 'busy',
|
|
95
|
-
OFFLINE: 'offline'
|
|
103
|
+
OFFLINE: 'offline',
|
|
104
|
+
LISTENING: 'listening'
|
|
96
105
|
};
|
|
97
106
|
|
|
98
107
|
// Proposal status
|
|
@@ -281,7 +290,7 @@ export function validateClientMessage(raw) {
|
|
|
281
290
|
if (msg.expires !== undefined && typeof msg.expires !== 'number') {
|
|
282
291
|
return { valid: false, error: 'expires must be a number (seconds)' };
|
|
283
292
|
}
|
|
284
|
-
if (msg.elo_stake !== undefined) {
|
|
293
|
+
if (msg.elo_stake !== undefined && msg.elo_stake !== null) {
|
|
285
294
|
if (typeof msg.elo_stake !== 'number' || msg.elo_stake < 0 || !Number.isInteger(msg.elo_stake)) {
|
|
286
295
|
return { valid: false, error: 'elo_stake must be a non-negative integer' };
|
|
287
296
|
}
|
|
@@ -297,7 +306,7 @@ export function validateClientMessage(raw) {
|
|
|
297
306
|
return { valid: false, error: 'Accept must be signed' };
|
|
298
307
|
}
|
|
299
308
|
// Optional: elo_stake for acceptor's stake
|
|
300
|
-
if (msg.elo_stake !== undefined) {
|
|
309
|
+
if (msg.elo_stake !== undefined && msg.elo_stake !== null) {
|
|
301
310
|
if (typeof msg.elo_stake !== 'number' || msg.elo_stake < 0 || !Number.isInteger(msg.elo_stake)) {
|
|
302
311
|
return { valid: false, error: 'elo_stake must be a non-negative integer' };
|
|
303
312
|
}
|
|
@@ -366,7 +375,7 @@ export function validateClientMessage(raw) {
|
|
|
366
375
|
|
|
367
376
|
case ClientMessageType.SET_PRESENCE:
|
|
368
377
|
// Set presence requires: status (online, away, busy, offline)
|
|
369
|
-
const validStatuses = ['online', 'away', 'busy', 'offline'];
|
|
378
|
+
const validStatuses = ['online', 'away', 'busy', 'offline', 'listening'];
|
|
370
379
|
if (!msg.status || !validStatuses.includes(msg.status)) {
|
|
371
380
|
return { valid: false, error: `Invalid presence status. Must be one of: ${validStatuses.join(', ')}` };
|
|
372
381
|
}
|
|
@@ -408,6 +417,30 @@ export function validateClientMessage(raw) {
|
|
|
408
417
|
}
|
|
409
418
|
break;
|
|
410
419
|
|
|
420
|
+
case ClientMessageType.ADMIN_APPROVE:
|
|
421
|
+
if (!msg.pubkey || typeof msg.pubkey !== 'string') {
|
|
422
|
+
return { valid: false, error: 'Missing or invalid pubkey' };
|
|
423
|
+
}
|
|
424
|
+
if (!msg.admin_key || typeof msg.admin_key !== 'string') {
|
|
425
|
+
return { valid: false, error: 'Missing admin_key' };
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
|
|
429
|
+
case ClientMessageType.ADMIN_REVOKE:
|
|
430
|
+
if (!msg.pubkey && !msg.agent_id) {
|
|
431
|
+
return { valid: false, error: 'Missing pubkey or agent_id' };
|
|
432
|
+
}
|
|
433
|
+
if (!msg.admin_key || typeof msg.admin_key !== 'string') {
|
|
434
|
+
return { valid: false, error: 'Missing admin_key' };
|
|
435
|
+
}
|
|
436
|
+
break;
|
|
437
|
+
|
|
438
|
+
case ClientMessageType.ADMIN_LIST:
|
|
439
|
+
if (!msg.admin_key || typeof msg.admin_key !== 'string') {
|
|
440
|
+
return { valid: false, error: 'Missing admin_key' };
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
|
|
411
444
|
default:
|
|
412
445
|
return { valid: false, error: `Unknown message type: ${msg.type}` };
|
|
413
446
|
}
|