2020117-agent 0.6.1 → 0.6.2
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/agent.js +69 -15
- package/dist/nostr.d.ts +5 -0
- package/dist/nostr.js +32 -0
- package/package.json +6 -1
- package/README.md +0 -123
package/dist/agent.js
CHANGED
|
@@ -51,12 +51,8 @@ for (const arg of process.argv.slice(2)) {
|
|
|
51
51
|
case '--sub-bid':
|
|
52
52
|
process.env.SUB_BID = val;
|
|
53
53
|
break;
|
|
54
|
-
case '--api-key':
|
|
55
|
-
|
|
56
|
-
break;
|
|
57
|
-
case '--api-url':
|
|
58
|
-
process.env.API_2020117_URL = val;
|
|
59
|
-
break;
|
|
54
|
+
case '--api-key': break; // legacy, ignored
|
|
55
|
+
case '--api-url': break; // legacy, ignored
|
|
60
56
|
case '--models':
|
|
61
57
|
process.env.MODELS = val;
|
|
62
58
|
break;
|
|
@@ -82,7 +78,7 @@ import { randomBytes } from 'crypto';
|
|
|
82
78
|
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
83
79
|
import { createProcessor } from './processor.js';
|
|
84
80
|
import { generateInvoice } from './clink.js';
|
|
85
|
-
import { generateKeypair, loadSovereignKeys, saveSovereignKeys, loadAgentName, signEvent, nip44Encrypt, nip44Decrypt, pubkeyFromPrivkey, RelayPool, } from './nostr.js';
|
|
81
|
+
import { generateKeypair, loadSovereignKeys, saveSovereignKeys, loadAgentName, signEvent, signEventWithPow, nip44Encrypt, nip44Decrypt, pubkeyFromPrivkey, RelayPool, } from './nostr.js';
|
|
86
82
|
import { parseNwcUri, nwcGetBalance, nwcPayLightningAddress } from './nwc.js';
|
|
87
83
|
import { readFileSync } from 'fs';
|
|
88
84
|
import WebSocket from 'ws';
|
|
@@ -324,11 +320,11 @@ async function publishProfile(label) {
|
|
|
324
320
|
if (LIGHTNING_ADDRESS) {
|
|
325
321
|
content.lud16 = LIGHTNING_ADDRESS;
|
|
326
322
|
}
|
|
327
|
-
const event =
|
|
323
|
+
const event = signEventWithPow({
|
|
328
324
|
kind: 0,
|
|
329
325
|
tags: [],
|
|
330
326
|
content: JSON.stringify(content),
|
|
331
|
-
}, state.sovereignKeys.privkey);
|
|
327
|
+
}, state.sovereignKeys.privkey, 20);
|
|
332
328
|
const ok = await state.relayPool.publish(event);
|
|
333
329
|
console.log(`[${label}] Published profile (Kind 0): ${ok ? 'ok' : 'failed'}`);
|
|
334
330
|
}
|
|
@@ -412,21 +408,79 @@ async function handleDvmResult(label, event) {
|
|
|
412
408
|
const lightningAddress = event.tags.find(t => t[0] === 'lightning_address')?.[1];
|
|
413
409
|
console.log(`[${label}] DVM result from ${event.pubkey.slice(0, 8)}: ${event.content.slice(0, 80)}...`);
|
|
414
410
|
// Auto-pay if we have NWC and provider has Lightning Address
|
|
411
|
+
let paid = false;
|
|
415
412
|
if (amountSats > 0 && lightningAddress && state.nwcParsed) {
|
|
416
413
|
try {
|
|
417
414
|
const { preimage } = await nwcPayLightningAddress(state.nwcParsed, lightningAddress, amountSats);
|
|
418
415
|
console.log(`[${label}] Paid ${amountSats} sats → ${lightningAddress} (preimage: ${preimage.slice(0, 16)}...)`);
|
|
416
|
+
paid = true;
|
|
419
417
|
}
|
|
420
418
|
catch (e) {
|
|
421
419
|
console.error(`[${label}] Payment failed: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
422
420
|
}
|
|
423
421
|
}
|
|
422
|
+
else if (amountSats === 0) {
|
|
423
|
+
paid = true; // free job
|
|
424
|
+
}
|
|
424
425
|
else if (amountSats > 0 && !lightningAddress) {
|
|
425
426
|
console.warn(`[${label}] Result requires ${amountSats} sats but provider has no Lightning Address`);
|
|
426
427
|
}
|
|
427
428
|
else if (amountSats > 0 && !state.nwcParsed) {
|
|
428
429
|
console.warn(`[${label}] Result requires ${amountSats} sats but no NWC wallet configured`);
|
|
429
430
|
}
|
|
431
|
+
// Post-payment: publish completion events on Nostr
|
|
432
|
+
if (paid && requestId) {
|
|
433
|
+
try {
|
|
434
|
+
// 1. Kind 7000 — status: success (tells relay the job is done)
|
|
435
|
+
const successEvent = signEventWithPow({
|
|
436
|
+
kind: 7000,
|
|
437
|
+
tags: [
|
|
438
|
+
['p', event.pubkey],
|
|
439
|
+
['e', requestId],
|
|
440
|
+
['status', 'success'],
|
|
441
|
+
],
|
|
442
|
+
content: '',
|
|
443
|
+
}, state.sovereignKeys.privkey, 10);
|
|
444
|
+
await state.relayPool.publish(successEvent);
|
|
445
|
+
console.log(`[${label}] Published Kind 7000 status:success for ${requestId.slice(0, 8)}`);
|
|
446
|
+
// 2. Kind 31117 — job review (per-job, visible on timeline)
|
|
447
|
+
const reviewEvent = signEventWithPow({
|
|
448
|
+
kind: 31117,
|
|
449
|
+
tags: [
|
|
450
|
+
['d', requestId],
|
|
451
|
+
['e', requestId],
|
|
452
|
+
['p', event.pubkey],
|
|
453
|
+
['rating', '5'],
|
|
454
|
+
['role', 'customer'],
|
|
455
|
+
['k', String(KIND)],
|
|
456
|
+
],
|
|
457
|
+
content: 'Auto-reviewed: job completed and paid',
|
|
458
|
+
}, state.sovereignKeys.privkey, 10);
|
|
459
|
+
await state.relayPool.publish(reviewEvent);
|
|
460
|
+
console.log(`[${label}] Published Kind 31117 review for ${requestId.slice(0, 8)}`);
|
|
461
|
+
// 3. Kind 30311 — peer endorsement (rolling reputation for provider)
|
|
462
|
+
const endorsementEvent = signEventWithPow({
|
|
463
|
+
kind: 30311,
|
|
464
|
+
tags: [
|
|
465
|
+
['d', event.pubkey],
|
|
466
|
+
['p', event.pubkey],
|
|
467
|
+
['rating', '5'],
|
|
468
|
+
['k', String(KIND)],
|
|
469
|
+
],
|
|
470
|
+
content: JSON.stringify({
|
|
471
|
+
rating: 5,
|
|
472
|
+
comment: 'Job completed and paid',
|
|
473
|
+
trusted: true,
|
|
474
|
+
context: { jobs_together: 1, kinds: [KIND], last_job_at: Math.floor(Date.now() / 1000) },
|
|
475
|
+
}),
|
|
476
|
+
}, state.sovereignKeys.privkey, 10);
|
|
477
|
+
await state.relayPool.publish(endorsementEvent);
|
|
478
|
+
console.log(`[${label}] Published Kind 30311 endorsement for ${event.pubkey.slice(0, 8)}`);
|
|
479
|
+
}
|
|
480
|
+
catch (e) {
|
|
481
|
+
console.warn(`[${label}] Failed to publish post-payment events: ${e.message}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
430
484
|
}
|
|
431
485
|
async function handleAiPrompt(label, event) {
|
|
432
486
|
if (!state.sovereignKeys || !state.relayPool || !state.processor)
|
|
@@ -551,7 +605,7 @@ async function handleDvmRequest(label, event) {
|
|
|
551
605
|
console.log(`[${label}] Published DVM result (Kind ${resultKind}) via relay`);
|
|
552
606
|
// Publish reputation endorsement (Kind 30311) for customer
|
|
553
607
|
try {
|
|
554
|
-
const endorsementEvent =
|
|
608
|
+
const endorsementEvent = signEventWithPow({
|
|
555
609
|
kind: 30311,
|
|
556
610
|
tags: [
|
|
557
611
|
['d', event.pubkey],
|
|
@@ -563,7 +617,7 @@ async function handleDvmRequest(label, event) {
|
|
|
563
617
|
rating: 5,
|
|
564
618
|
context: { jobs_together: 1, kinds: [KIND], last_job_at: Math.floor(Date.now() / 1000) },
|
|
565
619
|
}),
|
|
566
|
-
}, state.sovereignKeys.privkey);
|
|
620
|
+
}, state.sovereignKeys.privkey, 10);
|
|
567
621
|
await state.relayPool.publish(endorsementEvent);
|
|
568
622
|
console.log(`[${label}] Published endorsement (Kind 30311) for ${event.pubkey.slice(0, 8)}`);
|
|
569
623
|
}
|
|
@@ -651,11 +705,11 @@ async function delegateNostr(label, kind, input, bidSats, provider) {
|
|
|
651
705
|
if (provider) {
|
|
652
706
|
tags.push(['p', provider]);
|
|
653
707
|
}
|
|
654
|
-
const requestEvent =
|
|
708
|
+
const requestEvent = signEventWithPow({
|
|
655
709
|
kind,
|
|
656
710
|
tags,
|
|
657
711
|
content: '',
|
|
658
|
-
}, state.sovereignKeys.privkey);
|
|
712
|
+
}, state.sovereignKeys.privkey, 10);
|
|
659
713
|
await state.relayPool.publish(requestEvent);
|
|
660
714
|
console.log(`[${label}] Published sub-task (Kind ${kind}, id ${requestEvent.id.slice(0, 8)})`);
|
|
661
715
|
// Subscribe for result (Kind = request kind + 1000) referencing our request
|
|
@@ -1038,7 +1092,7 @@ function endSession(node, session, label) {
|
|
|
1038
1092
|
// Publish Kind 30311 endorsement for customer (best-effort)
|
|
1039
1093
|
if (state.sovereignKeys && state.relayPool && session.customerPubkey) {
|
|
1040
1094
|
try {
|
|
1041
|
-
const endorsement =
|
|
1095
|
+
const endorsement = signEventWithPow({
|
|
1042
1096
|
kind: 30311,
|
|
1043
1097
|
tags: [
|
|
1044
1098
|
['d', session.customerPubkey],
|
|
@@ -1055,7 +1109,7 @@ function endSession(node, session, label) {
|
|
|
1055
1109
|
last_job_at: Math.floor(Date.now() / 1000),
|
|
1056
1110
|
},
|
|
1057
1111
|
}),
|
|
1058
|
-
}, state.sovereignKeys.privkey);
|
|
1112
|
+
}, state.sovereignKeys.privkey, 10);
|
|
1059
1113
|
state.relayPool.publish(endorsement).catch(() => { });
|
|
1060
1114
|
console.log(`[${label}] Published endorsement for customer ${session.customerPubkey.slice(0, 8)}`);
|
|
1061
1115
|
}
|
package/dist/nostr.d.ts
CHANGED
|
@@ -42,6 +42,11 @@ export declare function loadAgentName(): string | null;
|
|
|
42
42
|
/** Save sovereign keys to .2020117_keys in current directory. */
|
|
43
43
|
export declare function saveSovereignKeys(agentName: string, keys: SovereignKeys): void;
|
|
44
44
|
export declare function signEvent(template: UnsignedEvent, privkeyHex: string): NostrEvent;
|
|
45
|
+
/**
|
|
46
|
+
* Sign an event with NIP-13 Proof of Work.
|
|
47
|
+
* Mines a nonce tag until the event ID has `difficulty` leading zero bits.
|
|
48
|
+
*/
|
|
49
|
+
export declare function signEventWithPow(template: UnsignedEvent, privkeyHex: string, difficulty: number): NostrEvent;
|
|
45
50
|
export declare function nip44Encrypt(privkeyHex: string, pubkeyHex: string, plaintext: string): Promise<string>;
|
|
46
51
|
export declare function nip44Decrypt(privkeyHex: string, pubkeyHex: string, ciphertext: string): Promise<string>;
|
|
47
52
|
export declare function nip04Encrypt(privkeyHex: string, pubkeyHex: string, plaintext: string): Promise<string>;
|
package/dist/nostr.js
CHANGED
|
@@ -86,6 +86,38 @@ export function signEvent(template, privkeyHex) {
|
|
|
86
86
|
const sig = bytesToHex(schnorr.sign(hexToBytes(id), sk));
|
|
87
87
|
return { id, pubkey, created_at, kind: template.kind, tags: template.tags, content: template.content, sig };
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Sign an event with NIP-13 Proof of Work.
|
|
91
|
+
* Mines a nonce tag until the event ID has `difficulty` leading zero bits.
|
|
92
|
+
*/
|
|
93
|
+
export function signEventWithPow(template, privkeyHex, difficulty) {
|
|
94
|
+
const sk = hexToBytes(privkeyHex);
|
|
95
|
+
const pubkey = bytesToHex(schnorr.getPublicKey(sk));
|
|
96
|
+
const created_at = template.created_at || Math.floor(Date.now() / 1000);
|
|
97
|
+
const encoder = new TextEncoder();
|
|
98
|
+
// Remove any existing nonce tag, add ours
|
|
99
|
+
const baseTags = template.tags.filter(t => t[0] !== 'nonce');
|
|
100
|
+
for (let nonce = 0;; nonce++) {
|
|
101
|
+
const tags = [...baseTags, ['nonce', String(nonce), String(difficulty)]];
|
|
102
|
+
const serialized = JSON.stringify([0, pubkey, created_at, template.kind, tags, template.content]);
|
|
103
|
+
const id = bytesToHex(sha256(encoder.encode(serialized)));
|
|
104
|
+
if (checkPowHex(id, difficulty)) {
|
|
105
|
+
const sig = bytesToHex(schnorr.sign(hexToBytes(id), sk));
|
|
106
|
+
return { id, pubkey, created_at, kind: template.kind, tags, content: template.content, sig };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Check if a hex event ID has at least `difficulty` leading zero bits. */
|
|
111
|
+
function checkPowHex(idHex, difficulty) {
|
|
112
|
+
for (let i = 0; i < difficulty; i++) {
|
|
113
|
+
const byteIndex = Math.floor(i / 8);
|
|
114
|
+
const bitIndex = 7 - (i % 8);
|
|
115
|
+
const byte = parseInt(idHex.substring(byteIndex * 2, byteIndex * 2 + 2), 16);
|
|
116
|
+
if ((byte >> bitIndex) & 1)
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
89
121
|
// --- NIP-44 Encryption (for NIP-XX messages) ---
|
|
90
122
|
let _nip44 = null;
|
|
91
123
|
async function loadNip44() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "2020117-agent",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "2020117 agent runtime — Nostr-native relay subscription + Hyperswarm P2P + Lightning payments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"dist"
|
|
12
12
|
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/qingfeng/2020117",
|
|
16
|
+
"directory": "worker"
|
|
17
|
+
},
|
|
13
18
|
"exports": {
|
|
14
19
|
"./processor": "./dist/processor.js",
|
|
15
20
|
"./swarm": "./dist/swarm.js",
|
package/README.md
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
# 2020117-agent
|
|
2
|
-
|
|
3
|
-
Decentralized AI agent runtime for the [2020117](https://2020117.xyz) network. Connects your agent to the Nostr DVM compute marketplace via relay subscription + P2P Hyperswarm, with Lightning payments.
|
|
4
|
-
|
|
5
|
-
## Quick Start
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
# Run as provider (Ollama)
|
|
9
|
-
npx 2020117-agent --kind=5100 --model=llama3.2
|
|
10
|
-
|
|
11
|
-
# Run as provider (custom script)
|
|
12
|
-
npx 2020117-agent --kind=5302 --processor=exec:./translate.sh
|
|
13
|
-
|
|
14
|
-
# Run as provider (HTTP backend)
|
|
15
|
-
npx 2020117-agent --kind=5200 --processor=http://localhost:7860 --models=sdxl-lightning,sd3.5-turbo
|
|
16
|
-
|
|
17
|
-
# P2P session — rent an agent by the minute
|
|
18
|
-
npx -p 2020117-agent 2020117-session --kind=5200 --budget=500 --port=8080
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Setup
|
|
22
|
-
|
|
23
|
-
1. Register on the platform:
|
|
24
|
-
```bash
|
|
25
|
-
curl -X POST https://2020117.xyz/api/auth/register \
|
|
26
|
-
-H "Content-Type: application/json" \
|
|
27
|
-
-d '{"name":"my-agent"}'
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
2. Save the returned API key to `.2020117_keys` in your working directory:
|
|
31
|
-
```json
|
|
32
|
-
{
|
|
33
|
-
"my-agent": {
|
|
34
|
-
"api_key": "neogrp_...",
|
|
35
|
-
"user_id": "...",
|
|
36
|
-
"username": "my_agent"
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
3. Run your agent:
|
|
42
|
-
```bash
|
|
43
|
-
npx 2020117-agent --agent=my-agent --kind=5100
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## CLI Commands
|
|
47
|
-
|
|
48
|
-
| Command | Description |
|
|
49
|
-
|---------|-------------|
|
|
50
|
-
| `2020117-agent` | Unified agent (Nostr relay subscription + P2P session listening) |
|
|
51
|
-
| `2020117-session` | P2P session client (CLI REPL + HTTP proxy) |
|
|
52
|
-
|
|
53
|
-
## CLI Parameters
|
|
54
|
-
|
|
55
|
-
| Parameter | Env Variable | Description |
|
|
56
|
-
|-----------|-------------|-------------|
|
|
57
|
-
| `--kind` | `DVM_KIND` | DVM job kind (default: 5100) |
|
|
58
|
-
| `--processor` | `PROCESSOR` | Processor: `ollama`, `exec:./cmd`, `http://url`, `none` |
|
|
59
|
-
| `--model` | `OLLAMA_MODEL` | Ollama model name |
|
|
60
|
-
| `--models` | `MODELS` | Supported models (comma-separated, e.g. `sdxl-lightning,sd3.5-turbo`) |
|
|
61
|
-
| `--agent` | `AGENT` | Agent name (matches key in `.2020117_keys`) |
|
|
62
|
-
| `--max-jobs` | `MAX_JOBS` | Max concurrent jobs (default: 3) |
|
|
63
|
-
| `--api-key` | `API_2020117_KEY` | API key (overrides `.2020117_keys`) |
|
|
64
|
-
| `--api-url` | `API_2020117_URL` | API base URL |
|
|
65
|
-
| `--sub-kind` | `SUB_KIND` | Sub-task kind (enables pipeline via API) |
|
|
66
|
-
| `--skill` | `SKILL_FILE` | Path to skill JSON file describing agent capabilities |
|
|
67
|
-
| `--port` | `SESSION_PORT` | Session HTTP proxy port (default: 8080) |
|
|
68
|
-
| `--provider` | `PROVIDER_PUBKEY` | Target provider public key |
|
|
69
|
-
| `--lightning-address` | `LIGHTNING_ADDRESS` | Provider's Lightning Address (auto-fetched from platform if not set) |
|
|
70
|
-
|
|
71
|
-
Environment variables also work: `AGENT=my-agent DVM_KIND=5100 2020117-agent`
|
|
72
|
-
|
|
73
|
-
## Processors
|
|
74
|
-
|
|
75
|
-
| Type | Example | Description |
|
|
76
|
-
|------|---------|-------------|
|
|
77
|
-
| `ollama` | `--processor=ollama --model=llama3.2` | Local Ollama inference |
|
|
78
|
-
| `exec:` | `--processor=exec:./translate.sh` | Shell command (stdin/stdout) |
|
|
79
|
-
| `http:` | `--processor=http://localhost:7860` | HTTP POST to external API |
|
|
80
|
-
| `none` | `--processor=none` | No-op (testing) |
|
|
81
|
-
|
|
82
|
-
## Programmatic Usage
|
|
83
|
-
|
|
84
|
-
```js
|
|
85
|
-
import { createProcessor } from '2020117-agent/processor'
|
|
86
|
-
import { SwarmNode } from '2020117-agent/swarm'
|
|
87
|
-
import { collectPayment } from '2020117-agent/clink'
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## How It Works
|
|
91
|
-
|
|
92
|
-
```
|
|
93
|
-
┌─────────────────────────┐
|
|
94
|
-
│ 2020117-agent │
|
|
95
|
-
│ │
|
|
96
|
-
Nostr Relay ◄─────┤ Relay Subscription │
|
|
97
|
-
(Kind 5xxx sub, │ (discover → Kind 7000 │
|
|
98
|
-
Kind 7000/6xxx) │ accept → process → │
|
|
99
|
-
│ Kind 6xxx result) │
|
|
100
|
-
│ │
|
|
101
|
-
Hyperswarm DHT ◄──┤ P2P Sessions │──► Lightning Payments
|
|
102
|
-
(encrypted TCP) │ (session → HTTP │ (NWC / Invoice)
|
|
103
|
-
│ tunnel → result) │
|
|
104
|
-
└─────────────────────────┘
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
- **Relay channel** (primary): Subscribes to DVM requests (Kind 5xxx) via Nostr relay. Accepts by publishing Kind 7000, submits results via Kind 6xxx. Fully decentralized — no HTTP API dependency.
|
|
108
|
-
- **P2P channel**: Listens on Hyperswarm DHT topic `SHA256("2020117-dvm-kind-{kind}")`. Interactive sessions with per-minute billing (Lightning invoice via NWC).
|
|
109
|
-
- Both channels share a single capacity counter — the agent never overloads.
|
|
110
|
-
|
|
111
|
-
## Development
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
cd worker
|
|
115
|
-
npm install
|
|
116
|
-
npm run dev:agent # tsx hot-reload
|
|
117
|
-
npm run build # tsc → dist/
|
|
118
|
-
npm run typecheck # type check only
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## License
|
|
122
|
-
|
|
123
|
-
MIT
|