agent-escrow 0.1.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/LICENSE +21 -0
- package/PROTOCOL.md +180 -0
- package/README.md +179 -0
- package/package.json +57 -0
- package/src/bid.cjs +92 -0
- package/src/cli.cjs +263 -0
- package/src/delivery.cjs +85 -0
- package/src/escrow.cjs +97 -0
- package/src/index.cjs +50 -0
- package/src/marketplace.cjs +362 -0
- package/src/nostr.cjs +150 -0
- package/src/resolution.cjs +80 -0
- package/src/task.cjs +148 -0
- package/src/trust.cjs +120 -0
package/src/task.cjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { KINDS, TASK_STATUS, signEvent, publishToRelays, queryRelays, getTag, getTags } = require('./nostr.cjs');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a task event template
|
|
8
|
+
*/
|
|
9
|
+
function createTaskEvent(opts) {
|
|
10
|
+
const {
|
|
11
|
+
title,
|
|
12
|
+
description,
|
|
13
|
+
budget,
|
|
14
|
+
deadline,
|
|
15
|
+
capabilities = [],
|
|
16
|
+
minTrust,
|
|
17
|
+
lightningAddress,
|
|
18
|
+
taskId
|
|
19
|
+
} = opts;
|
|
20
|
+
|
|
21
|
+
if (!title) throw new Error('title is required');
|
|
22
|
+
if (!budget || budget <= 0) throw new Error('budget must be positive');
|
|
23
|
+
|
|
24
|
+
const id = taskId || crypto.randomUUID();
|
|
25
|
+
|
|
26
|
+
const tags = [
|
|
27
|
+
['d', id],
|
|
28
|
+
['title', title],
|
|
29
|
+
['budget', String(budget)],
|
|
30
|
+
['status', TASK_STATUS.OPEN]
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
if (deadline) tags.push(['deadline', String(Math.floor(deadline / 1000))]);
|
|
34
|
+
for (const cap of capabilities) tags.push(['c', cap]);
|
|
35
|
+
if (minTrust) tags.push(['min-trust', String(minTrust)]);
|
|
36
|
+
if (lightningAddress) tags.push(['ln', lightningAddress]);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
kind: KINDS.TASK,
|
|
40
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
41
|
+
tags,
|
|
42
|
+
content: description || ''
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create an updated task event (same d-tag, new status)
|
|
48
|
+
*/
|
|
49
|
+
function updateTaskEvent(originalEvent, updates) {
|
|
50
|
+
const tags = originalEvent.tags.map(t => {
|
|
51
|
+
if (t[0] === 'status' && updates.status) return ['status', updates.status];
|
|
52
|
+
return [...t];
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Add worker pubkey on claim
|
|
56
|
+
if (updates.workerPubkey && !tags.find(t => t[0] === 'p')) {
|
|
57
|
+
tags.push(['p', updates.workerPubkey]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
kind: KINDS.TASK,
|
|
62
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
63
|
+
tags,
|
|
64
|
+
content: updates.content || originalEvent.content
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse a task event into a structured object
|
|
70
|
+
*/
|
|
71
|
+
function parseTask(event) {
|
|
72
|
+
return {
|
|
73
|
+
eventId: event.id,
|
|
74
|
+
taskId: getTag(event, 'd'),
|
|
75
|
+
poster: event.pubkey,
|
|
76
|
+
title: getTag(event, 'title'),
|
|
77
|
+
description: event.content,
|
|
78
|
+
budget: parseInt(getTag(event, 'budget') || '0', 10),
|
|
79
|
+
deadline: getTag(event, 'deadline') ? parseInt(getTag(event, 'deadline'), 10) * 1000 : null,
|
|
80
|
+
status: getTag(event, 'status') || TASK_STATUS.OPEN,
|
|
81
|
+
capabilities: getTags(event, 'c'),
|
|
82
|
+
minTrust: getTag(event, 'min-trust') ? parseInt(getTag(event, 'min-trust'), 10) : null,
|
|
83
|
+
lightningAddress: getTag(event, 'ln'),
|
|
84
|
+
worker: getTag(event, 'p'),
|
|
85
|
+
createdAt: event.created_at * 1000,
|
|
86
|
+
raw: event
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build filters to find tasks on relays
|
|
92
|
+
*/
|
|
93
|
+
function buildTaskFilters(opts = {}) {
|
|
94
|
+
const filter = { kinds: [KINDS.TASK] };
|
|
95
|
+
|
|
96
|
+
if (opts.status) filter['#status'] = Array.isArray(opts.status) ? opts.status : [opts.status];
|
|
97
|
+
if (opts.capabilities) filter['#c'] = Array.isArray(opts.capabilities) ? opts.capabilities : [opts.capabilities];
|
|
98
|
+
if (opts.poster) filter.authors = Array.isArray(opts.poster) ? opts.poster : [opts.poster];
|
|
99
|
+
if (opts.limit) filter.limit = opts.limit;
|
|
100
|
+
if (opts.since) filter.since = Math.floor(opts.since / 1000);
|
|
101
|
+
|
|
102
|
+
return filter;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Query relays for tasks
|
|
107
|
+
*/
|
|
108
|
+
async function findTasks(relays, opts = {}) {
|
|
109
|
+
const filter = buildTaskFilters(opts);
|
|
110
|
+
const events = await queryRelays(relays, filter, opts.timeoutMs);
|
|
111
|
+
|
|
112
|
+
let tasks = events.map(parseTask);
|
|
113
|
+
|
|
114
|
+
// Client-side filtering for things relays can't filter
|
|
115
|
+
if (opts.minBudget) tasks = tasks.filter(t => t.budget >= opts.minBudget);
|
|
116
|
+
if (opts.maxBudget) tasks = tasks.filter(t => t.budget <= opts.maxBudget);
|
|
117
|
+
|
|
118
|
+
// Sort by creation time, newest first
|
|
119
|
+
tasks.sort((a, b) => b.createdAt - a.createdAt);
|
|
120
|
+
|
|
121
|
+
return tasks;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get a specific task by its d-tag (taskId)
|
|
126
|
+
*/
|
|
127
|
+
async function getTask(relays, posterPubkey, taskId, timeoutMs = 8000) {
|
|
128
|
+
const filter = {
|
|
129
|
+
kinds: [KINDS.TASK],
|
|
130
|
+
authors: [posterPubkey],
|
|
131
|
+
'#d': [taskId]
|
|
132
|
+
};
|
|
133
|
+
const events = await queryRelays(relays, filter, timeoutMs);
|
|
134
|
+
if (events.length === 0) return null;
|
|
135
|
+
|
|
136
|
+
// Parameterized replaceable: latest event wins
|
|
137
|
+
events.sort((a, b) => b.created_at - a.created_at);
|
|
138
|
+
return parseTask(events[0]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
createTaskEvent,
|
|
143
|
+
updateTaskEvent,
|
|
144
|
+
parseTask,
|
|
145
|
+
buildTaskFilters,
|
|
146
|
+
findTasks,
|
|
147
|
+
getTask
|
|
148
|
+
};
|
package/src/trust.cjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Trust integration — optional ai.wot dependency
|
|
5
|
+
*
|
|
6
|
+
* Falls back gracefully if ai-wot is not installed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
let aiWot = null;
|
|
10
|
+
try {
|
|
11
|
+
aiWot = require('ai-wot');
|
|
12
|
+
} catch (e) {
|
|
13
|
+
// ai-wot not installed — trust features disabled
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if trust features are available
|
|
18
|
+
*/
|
|
19
|
+
function trustAvailable() {
|
|
20
|
+
return aiWot !== null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get trust score for a pubkey
|
|
25
|
+
* Returns null if ai-wot is not installed
|
|
26
|
+
*/
|
|
27
|
+
async function getTrustScore(pubkey, opts = {}) {
|
|
28
|
+
if (!aiWot) return null;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const relayUrls = opts.relays || ['wss://relay.damus.io', 'wss://nos.lol'];
|
|
32
|
+
const score = await aiWot.calculateTrustScore(pubkey, { relayUrls });
|
|
33
|
+
return score;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if a pubkey meets minimum trust threshold
|
|
41
|
+
* Returns true if ai-wot is not installed (permissive default)
|
|
42
|
+
*/
|
|
43
|
+
async function meetsTrustThreshold(pubkey, minScore, opts = {}) {
|
|
44
|
+
if (!aiWot) return true; // No trust system = trust everyone
|
|
45
|
+
if (!minScore || minScore <= 0) return true;
|
|
46
|
+
|
|
47
|
+
const score = await getTrustScore(pubkey, opts);
|
|
48
|
+
if (score === null) return true; // Can't check = allow
|
|
49
|
+
return score.total >= minScore;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Publish a work-completed attestation
|
|
54
|
+
* Returns null if ai-wot is not installed
|
|
55
|
+
*/
|
|
56
|
+
async function publishWorkAttestation(opts = {}) {
|
|
57
|
+
if (!aiWot) return null;
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
targetPubkey,
|
|
61
|
+
comment,
|
|
62
|
+
secretKey,
|
|
63
|
+
relayUrls = ['wss://relay.damus.io', 'wss://nos.lol']
|
|
64
|
+
} = opts;
|
|
65
|
+
|
|
66
|
+
if (!targetPubkey) throw new Error('targetPubkey is required');
|
|
67
|
+
if (!secretKey) throw new Error('secretKey is required');
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const attestation = await aiWot.publishAttestation({
|
|
71
|
+
targetPubkey,
|
|
72
|
+
type: 'work-completed',
|
|
73
|
+
comment: comment || 'Task completed via agent-escrow marketplace',
|
|
74
|
+
secretKey,
|
|
75
|
+
relayUrls
|
|
76
|
+
});
|
|
77
|
+
return attestation;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// Non-fatal — trust attestation is a bonus, not a requirement
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Publish a negative attestation (for disputes)
|
|
86
|
+
*/
|
|
87
|
+
async function publishDisputeAttestation(opts = {}) {
|
|
88
|
+
if (!aiWot) return null;
|
|
89
|
+
|
|
90
|
+
const {
|
|
91
|
+
targetPubkey,
|
|
92
|
+
comment,
|
|
93
|
+
secretKey,
|
|
94
|
+
relayUrls = ['wss://relay.damus.io', 'wss://nos.lol']
|
|
95
|
+
} = opts;
|
|
96
|
+
|
|
97
|
+
if (!targetPubkey) throw new Error('targetPubkey is required');
|
|
98
|
+
if (!secretKey) throw new Error('secretKey is required');
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const attestation = await aiWot.publishAttestation({
|
|
102
|
+
targetPubkey,
|
|
103
|
+
type: 'service-quality',
|
|
104
|
+
comment: comment || 'Dispute filed via agent-escrow marketplace',
|
|
105
|
+
secretKey,
|
|
106
|
+
relayUrls
|
|
107
|
+
});
|
|
108
|
+
return attestation;
|
|
109
|
+
} catch (e) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
trustAvailable,
|
|
116
|
+
getTrustScore,
|
|
117
|
+
meetsTrustThreshold,
|
|
118
|
+
publishWorkAttestation,
|
|
119
|
+
publishDisputeAttestation
|
|
120
|
+
};
|