@highway1/core 0.1.39 → 0.1.41
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/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +18 -4
- package/src/discovery/agent-card-encoder.ts +119 -0
- package/src/discovery/agent-card-schema.ts +87 -0
- package/src/discovery/agent-card-types.ts +99 -0
- package/src/discovery/agent-card.ts +190 -0
- package/src/discovery/bootstrap.ts +63 -0
- package/src/discovery/capability-matcher.ts +167 -0
- package/src/discovery/dht.ts +285 -0
- package/src/discovery/index.ts +3 -0
- package/src/discovery/search-index.ts +247 -0
- package/src/discovery/semantic-search.ts +218 -0
- package/src/identity/did.ts +48 -0
- package/src/identity/document.ts +77 -0
- package/src/identity/index.ts +4 -0
- package/src/identity/keys.ts +79 -0
- package/src/identity/signer.ts +55 -0
- package/src/index.ts +33 -0
- package/src/messaging/codec.ts +47 -0
- package/src/messaging/envelope.ts +107 -0
- package/src/messaging/index.ts +3 -0
- package/src/messaging/router.ts +368 -0
- package/src/transport/connection.ts +77 -0
- package/src/transport/index.ts +2 -0
- package/src/transport/node.ts +152 -0
- package/src/trust/endorsement.ts +167 -0
- package/src/trust/index.ts +194 -0
- package/src/trust/interaction-history.ts +155 -0
- package/src/trust/sybil-defense.ts +232 -0
- package/src/trust/trust-score.ts +136 -0
- package/src/utils/errors.ts +38 -0
- package/src/utils/logger.ts +48 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sybil Defense Mechanisms
|
|
3
|
+
*
|
|
4
|
+
* Protects the network from Sybil attacks through:
|
|
5
|
+
* - Entry cost (Hashcash challenges)
|
|
6
|
+
* - Progressive trust (rate limiting for new agents)
|
|
7
|
+
* - DHT region protection (prefer established peers)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createLogger } from '../utils/logger.js';
|
|
11
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
12
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
13
|
+
|
|
14
|
+
const logger = createLogger('sybil-defense');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Hashcash Challenge
|
|
18
|
+
*/
|
|
19
|
+
export interface Challenge {
|
|
20
|
+
did: string;
|
|
21
|
+
difficulty: number; // Number of leading zero bits required
|
|
22
|
+
nonce: string;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Challenge Solution
|
|
28
|
+
*/
|
|
29
|
+
export interface ChallengeSolution {
|
|
30
|
+
challenge: Challenge;
|
|
31
|
+
solution: string; // Nonce that produces required hash
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Rate Limit Record
|
|
36
|
+
*/
|
|
37
|
+
interface RateLimitRecord {
|
|
38
|
+
did: string;
|
|
39
|
+
requests: number[]; // Timestamps of requests
|
|
40
|
+
firstSeen: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Peer Trust Level
|
|
45
|
+
*/
|
|
46
|
+
export type PeerTrustLevel = 'new' | 'established' | 'trusted';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Sybil Defense Manager
|
|
50
|
+
*/
|
|
51
|
+
export class SybilDefense {
|
|
52
|
+
private rateLimits = new Map<string, RateLimitRecord>();
|
|
53
|
+
private peerFirstSeen = new Map<string, number>();
|
|
54
|
+
private readonly NEW_AGENT_WINDOW = 24 * 60 * 60 * 1000; // 24 hours
|
|
55
|
+
private readonly RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour
|
|
56
|
+
private readonly MAX_REQUESTS_NEW = 10; // Max requests per hour for new agents
|
|
57
|
+
private readonly ESTABLISHED_THRESHOLD = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate a Hashcash challenge for a new DID
|
|
61
|
+
*/
|
|
62
|
+
generateChallenge(did: string, difficulty = 20): Challenge {
|
|
63
|
+
const nonce = this.generateNonce();
|
|
64
|
+
return {
|
|
65
|
+
did,
|
|
66
|
+
difficulty,
|
|
67
|
+
nonce,
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Verify a Hashcash challenge solution
|
|
74
|
+
*/
|
|
75
|
+
verifyChallenge(solution: ChallengeSolution): boolean {
|
|
76
|
+
const { challenge, solution: solutionNonce } = solution;
|
|
77
|
+
|
|
78
|
+
// Check challenge is not too old (valid for 1 hour)
|
|
79
|
+
if (Date.now() - challenge.timestamp > 60 * 60 * 1000) {
|
|
80
|
+
logger.warn('Challenge expired', { did: challenge.did });
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Compute hash
|
|
85
|
+
const data = `${challenge.did}:${challenge.nonce}:${solutionNonce}`;
|
|
86
|
+
const hash = sha256(new TextEncoder().encode(data));
|
|
87
|
+
|
|
88
|
+
// Check leading zeros
|
|
89
|
+
const leadingZeros = this.countLeadingZeroBits(hash);
|
|
90
|
+
const valid = leadingZeros >= challenge.difficulty;
|
|
91
|
+
|
|
92
|
+
if (valid) {
|
|
93
|
+
logger.info('Challenge verified', { did: challenge.did, leadingZeros });
|
|
94
|
+
} else {
|
|
95
|
+
logger.warn('Challenge failed', { did: challenge.did, leadingZeros, required: challenge.difficulty });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return valid;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if an agent should be rate limited
|
|
103
|
+
*/
|
|
104
|
+
isRateLimited(did: string): boolean {
|
|
105
|
+
const record = this.rateLimits.get(did);
|
|
106
|
+
if (!record) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
const agentAge = now - record.firstSeen;
|
|
112
|
+
|
|
113
|
+
// Only rate limit new agents
|
|
114
|
+
if (agentAge > this.NEW_AGENT_WINDOW) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Count recent requests
|
|
119
|
+
const recentRequests = record.requests.filter(
|
|
120
|
+
t => now - t < this.RATE_LIMIT_WINDOW
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return recentRequests.length >= this.MAX_REQUESTS_NEW;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Record a request from an agent
|
|
128
|
+
*/
|
|
129
|
+
recordRequest(did: string): void {
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
let record = this.rateLimits.get(did);
|
|
132
|
+
|
|
133
|
+
if (!record) {
|
|
134
|
+
record = {
|
|
135
|
+
did,
|
|
136
|
+
requests: [],
|
|
137
|
+
firstSeen: now,
|
|
138
|
+
};
|
|
139
|
+
this.rateLimits.set(did, record);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Add request timestamp
|
|
143
|
+
record.requests.push(now);
|
|
144
|
+
|
|
145
|
+
// Clean up old requests
|
|
146
|
+
record.requests = record.requests.filter(
|
|
147
|
+
t => now - t < this.RATE_LIMIT_WINDOW
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
logger.debug('Recorded request', { did, count: record.requests.length });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get peer trust level based on age
|
|
155
|
+
*/
|
|
156
|
+
getPeerTrustLevel(peerId: string): PeerTrustLevel {
|
|
157
|
+
const firstSeen = this.peerFirstSeen.get(peerId);
|
|
158
|
+
if (!firstSeen) {
|
|
159
|
+
return 'new';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const age = Date.now() - firstSeen;
|
|
163
|
+
|
|
164
|
+
if (age < this.NEW_AGENT_WINDOW) {
|
|
165
|
+
return 'new';
|
|
166
|
+
} else if (age < this.ESTABLISHED_THRESHOLD) {
|
|
167
|
+
return 'established';
|
|
168
|
+
} else {
|
|
169
|
+
return 'trusted';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Record first seen time for a peer
|
|
175
|
+
*/
|
|
176
|
+
recordPeerSeen(peerId: string): void {
|
|
177
|
+
if (!this.peerFirstSeen.has(peerId)) {
|
|
178
|
+
this.peerFirstSeen.set(peerId, Date.now());
|
|
179
|
+
logger.debug('Recorded new peer', { peerId });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Clean up old records
|
|
185
|
+
*/
|
|
186
|
+
cleanup(): void {
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
const cutoff = now - this.NEW_AGENT_WINDOW;
|
|
189
|
+
|
|
190
|
+
// Clean up rate limits for old agents
|
|
191
|
+
for (const [did, record] of this.rateLimits.entries()) {
|
|
192
|
+
if (record.firstSeen < cutoff) {
|
|
193
|
+
this.rateLimits.delete(did);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
logger.info('Cleaned up Sybil defense records', {
|
|
198
|
+
rateLimits: this.rateLimits.size,
|
|
199
|
+
peers: this.peerFirstSeen.size,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Generate a random nonce
|
|
205
|
+
*/
|
|
206
|
+
private generateNonce(): string {
|
|
207
|
+
const bytes = new Uint8Array(16);
|
|
208
|
+
crypto.getRandomValues(bytes);
|
|
209
|
+
return bytesToHex(bytes);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Count leading zero bits in a hash
|
|
214
|
+
*/
|
|
215
|
+
private countLeadingZeroBits(hash: Uint8Array): number {
|
|
216
|
+
let count = 0;
|
|
217
|
+
for (const byte of hash) {
|
|
218
|
+
if (byte === 0) {
|
|
219
|
+
count += 8;
|
|
220
|
+
} else {
|
|
221
|
+
// Count leading zeros in this byte
|
|
222
|
+
let b = byte;
|
|
223
|
+
while ((b & 0x80) === 0) {
|
|
224
|
+
count++;
|
|
225
|
+
b <<= 1;
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return count;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust Score System
|
|
3
|
+
*
|
|
4
|
+
* Tracks agent reputation based on interactions, endorsements,
|
|
5
|
+
* and network behavior.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Trust Score Metrics
|
|
10
|
+
*/
|
|
11
|
+
export interface TrustScore {
|
|
12
|
+
interactionScore: number; // 0-1, based on successful interactions
|
|
13
|
+
endorsements: number; // Count of endorsements
|
|
14
|
+
completionRate: number; // % of completed tasks (0-1)
|
|
15
|
+
responseTime: number; // Average response time (ms)
|
|
16
|
+
uptime: number; // % of time online last 30 days (0-1)
|
|
17
|
+
lastUpdated: number; // Timestamp
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Interaction Record
|
|
22
|
+
*/
|
|
23
|
+
export interface Interaction {
|
|
24
|
+
agentDid: string;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
type: 'message' | 'task' | 'query';
|
|
27
|
+
success: boolean;
|
|
28
|
+
responseTime: number;
|
|
29
|
+
rating?: number; // 1-5 stars (optional user rating)
|
|
30
|
+
feedback?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Interaction Statistics
|
|
35
|
+
*/
|
|
36
|
+
export interface InteractionStats {
|
|
37
|
+
totalInteractions: number;
|
|
38
|
+
successRate: number;
|
|
39
|
+
avgResponseTime: number;
|
|
40
|
+
lastInteraction: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Trust Metrics Calculator
|
|
45
|
+
*/
|
|
46
|
+
export class TrustMetrics {
|
|
47
|
+
/**
|
|
48
|
+
* Calculate trust score from interaction history
|
|
49
|
+
*/
|
|
50
|
+
calculateScore(stats: InteractionStats, endorsements: number, uptime: number): TrustScore {
|
|
51
|
+
// Interaction score: weighted by success rate and volume
|
|
52
|
+
const volumeWeight = Math.min(stats.totalInteractions / 100, 1); // Cap at 100 interactions
|
|
53
|
+
const interactionScore = stats.successRate * volumeWeight;
|
|
54
|
+
|
|
55
|
+
// Completion rate is same as success rate for now
|
|
56
|
+
const completionRate = stats.successRate;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
interactionScore,
|
|
60
|
+
endorsements,
|
|
61
|
+
completionRate,
|
|
62
|
+
responseTime: stats.avgResponseTime,
|
|
63
|
+
uptime,
|
|
64
|
+
lastUpdated: Date.now(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Calculate overall trust level (0-1)
|
|
70
|
+
*/
|
|
71
|
+
calculateOverallTrust(score: TrustScore): number {
|
|
72
|
+
// Weighted average of different factors
|
|
73
|
+
const weights = {
|
|
74
|
+
interaction: 0.4,
|
|
75
|
+
endorsement: 0.2,
|
|
76
|
+
completion: 0.2,
|
|
77
|
+
uptime: 0.2,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const endorsementScore = Math.min(score.endorsements / 10, 1); // Cap at 10 endorsements
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
score.interactionScore * weights.interaction +
|
|
84
|
+
endorsementScore * weights.endorsement +
|
|
85
|
+
score.completionRate * weights.completion +
|
|
86
|
+
score.uptime * weights.uptime
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get trust level category
|
|
92
|
+
*/
|
|
93
|
+
getTrustLevel(score: TrustScore): 'new' | 'low' | 'medium' | 'high' | 'trusted' {
|
|
94
|
+
const overall = this.calculateOverallTrust(score);
|
|
95
|
+
|
|
96
|
+
if (score.interactionScore === 0) return 'new';
|
|
97
|
+
if (overall < 0.3) return 'low';
|
|
98
|
+
if (overall < 0.6) return 'medium';
|
|
99
|
+
if (overall < 0.8) return 'high';
|
|
100
|
+
return 'trusted';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if agent should be rate limited
|
|
105
|
+
*/
|
|
106
|
+
shouldRateLimit(score: TrustScore, agentAge: number): boolean {
|
|
107
|
+
const overall = this.calculateOverallTrust(score);
|
|
108
|
+
const ONE_DAY = 24 * 60 * 60 * 1000;
|
|
109
|
+
|
|
110
|
+
// New agents (< 24 hours) with low trust
|
|
111
|
+
if (agentAge < ONE_DAY && overall < 0.3) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Very low trust regardless of age
|
|
116
|
+
if (overall < 0.1) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Default trust score for new agents
|
|
126
|
+
*/
|
|
127
|
+
export function createDefaultTrustScore(): TrustScore {
|
|
128
|
+
return {
|
|
129
|
+
interactionScore: 0,
|
|
130
|
+
endorsements: 0,
|
|
131
|
+
completionRate: 0,
|
|
132
|
+
responseTime: 0,
|
|
133
|
+
uptime: 1.0, // Assume online initially
|
|
134
|
+
lastUpdated: Date.now(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class ClawiverseError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
message: string,
|
|
4
|
+
public code: string,
|
|
5
|
+
public details?: unknown
|
|
6
|
+
) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'ClawiverseError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class IdentityError extends ClawiverseError {
|
|
13
|
+
constructor(message: string, details?: unknown) {
|
|
14
|
+
super(message, 'IDENTITY_ERROR', details);
|
|
15
|
+
this.name = 'IdentityError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class TransportError extends ClawiverseError {
|
|
20
|
+
constructor(message: string, details?: unknown) {
|
|
21
|
+
super(message, 'TRANSPORT_ERROR', details);
|
|
22
|
+
this.name = 'TransportError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class DiscoveryError extends ClawiverseError {
|
|
27
|
+
constructor(message: string, details?: unknown) {
|
|
28
|
+
super(message, 'DISCOVERY_ERROR', details);
|
|
29
|
+
this.name = 'DiscoveryError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class MessagingError extends ClawiverseError {
|
|
34
|
+
constructor(message: string, details?: unknown) {
|
|
35
|
+
super(message, 'MESSAGING_ERROR', details);
|
|
36
|
+
this.name = 'MessagingError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export enum LogLevel {
|
|
2
|
+
DEBUG = 0,
|
|
3
|
+
INFO = 1,
|
|
4
|
+
WARN = 2,
|
|
5
|
+
ERROR = 3,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class Logger {
|
|
9
|
+
private level: LogLevel;
|
|
10
|
+
private prefix: string;
|
|
11
|
+
|
|
12
|
+
constructor(prefix: string, level: LogLevel = LogLevel.INFO) {
|
|
13
|
+
this.prefix = prefix;
|
|
14
|
+
this.level = level;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setLevel(level: LogLevel): void {
|
|
18
|
+
this.level = level;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
debug(message: string, ...args: unknown[]): void {
|
|
22
|
+
if (this.level <= LogLevel.DEBUG) {
|
|
23
|
+
console.debug(`[${this.prefix}] DEBUG:`, message, ...args);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
info(message: string, ...args: unknown[]): void {
|
|
28
|
+
if (this.level <= LogLevel.INFO) {
|
|
29
|
+
console.info(`[${this.prefix}] INFO:`, message, ...args);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
warn(message: string, ...args: unknown[]): void {
|
|
34
|
+
if (this.level <= LogLevel.WARN) {
|
|
35
|
+
console.warn(`[${this.prefix}] WARN:`, message, ...args);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
error(message: string, ...args: unknown[]): void {
|
|
40
|
+
if (this.level <= LogLevel.ERROR) {
|
|
41
|
+
console.error(`[${this.prefix}] ERROR:`, message, ...args);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const createLogger = (prefix: string, level?: LogLevel): Logger => {
|
|
47
|
+
return new Logger(prefix, level);
|
|
48
|
+
};
|