clinch-core 0.6.2 → 0.7.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/README.md +52 -4
- package/dist/index.d.ts +11 -0
- package/dist/index.js +122 -7
- package/package.json +1 -1
- package/src/index.ts +157 -14
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
The Agent Negotiation Protocol (ANP) governs how two autonomous software agents establish identity, declare capabilities, and reach a cryptographically verifiable agreement on price and terms.
|
|
12
12
|
|
|
13
13
|
### 1.1 Address Formatting & Routing
|
|
14
|
-
All destination routing in Clinch relies on
|
|
14
|
+
All destination routing in Clinch relies on structured, prefixed addresses:
|
|
15
15
|
|
|
16
16
|
$$\text{PROTOCOL\_MODE} \mathbin{.} \text{domain} \mathbin{.} \text{TLD}$$
|
|
17
17
|
|
|
@@ -36,9 +36,10 @@ The SDK maintains an isolated, turn-based state machine for each active session.
|
|
|
36
36
|
* `OFFLINE`: Client is completely disconnected.
|
|
37
37
|
* `CONNECTING`: Authenticating identity and performing local Proof-of-Work (PoW) verification.
|
|
38
38
|
* `IDLE`: Connected and authenticated. Ready to initialize new sessions.
|
|
39
|
+
* `RECONNECTING`: Socket connection lost; executing exponential backoff.
|
|
39
40
|
* `NEGOTIATING`: Active, turn-based bargaining sequence in progress.
|
|
40
41
|
* `CONVERGED`: Mathematical convergence reached; agreement co-signed.
|
|
41
|
-
* `STALEMATE`:
|
|
42
|
+
* `STALEMATE`: Negotiation terminated; max turns reached without convergence.
|
|
42
43
|
* `ERROR`: Internal network, compilation, or cryptographic validation failure.
|
|
43
44
|
|
|
44
45
|
### Developer Event Subscriptions
|
|
@@ -114,7 +115,7 @@ async function runAutonomousSession() {
|
|
|
114
115
|
runAutonomousSession();
|
|
115
116
|
```
|
|
116
117
|
|
|
117
|
-
### 4.2
|
|
118
|
+
### 4.2 Webhook & Horizontal Scale Pattern
|
|
118
119
|
For enterprise cloud environments where processes must remain stateless or scale horizontally across server pods, you must serialize and rehydrate active sessions dynamically. This ensures that when an asynchronous callback arrives, any pod can load the session state and use the matching ephemeral key to sign the next turn.
|
|
119
120
|
|
|
120
121
|
```javascript
|
|
@@ -167,11 +168,51 @@ webhookCore.on('callback_received', async (event) => {
|
|
|
167
168
|
});
|
|
168
169
|
```
|
|
169
170
|
|
|
171
|
+
### 4.3 Cascading Multi-Seller Squeeze and Races
|
|
172
|
+
To implement market discovery, the SDK supports cascading negotiations across multiple matching sellers. The Core orchestrates two distinct optimization strategies:
|
|
173
|
+
|
|
174
|
+
#### Sequential Squeeze (Default)
|
|
175
|
+
Useful for high-value items where time is secondary to price. The SDK negotiates sequentially with each seller, using the converged deal price of the previous seller as the strict, maximum budget ceiling for the next. This dynamically forces sellers to underbid one another.
|
|
176
|
+
|
|
177
|
+
#### Parallel Race
|
|
178
|
+
Necessary for real-time services like ride-hailing or logistics. The SDK handshakes and conducts negotiations with all selected sellers concurrently in parallel. Once all sessions finish, it selects the absolute lowest price converged under your budget.
|
|
179
|
+
|
|
180
|
+
```javascript
|
|
181
|
+
// Triggers the cascading loop
|
|
182
|
+
const bestDeal = await core.negotiateCascade(
|
|
183
|
+
'domain_name', // Category to discover
|
|
184
|
+
{ intent: 'purchase', item: 'mybrand.io', max_budget: 150.00 },
|
|
185
|
+
3, // Max sellers to target
|
|
186
|
+
'sequential' // 'sequential' | 'parallel'
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (bestDeal) {
|
|
190
|
+
console.log(`🏆 Optimal deal secured with ${bestDeal.sellerId} at $${bestDeal.finalPrice}`);
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 4.4 Blind Key Pass (Credential Injection)
|
|
195
|
+
For sessions with services that require authorization tokens or API keys to operate (such as platform execution nodes), you can register these credentials locally.
|
|
196
|
+
|
|
197
|
+
The Core library securely stores these secrets and silently injects them into the handshake transport layer during session initialization. This prevents the API keys from ever being exposed to the AI model's context window, safeguarding you against prompt injection attacks.
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
// Register the API credential locally bound to the domain namespace
|
|
201
|
+
core.registerSecret('apify.anp', 'apify_sec_key_xyz987', 'Apify Production Token');
|
|
202
|
+
|
|
203
|
+
// Handshaking with this address now automatically injects the credential silently
|
|
204
|
+
const sessionId = await core.negotiate('ANP/C.apify.anp', {
|
|
205
|
+
intent: 'purchase',
|
|
206
|
+
item: 'actor-scraper-run',
|
|
207
|
+
max_budget: 5.00
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
170
211
|
---
|
|
171
212
|
|
|
172
213
|
## 5. Integration Guide: Seller Nodes
|
|
173
214
|
|
|
174
|
-
Sellers use the `ClinchSeller` class to build compliant, automated endpoints.
|
|
215
|
+
Sellers use the `ClinchSeller` class to build compliant, automated endpoints.
|
|
175
216
|
|
|
176
217
|
### 5.1 Decoupled Routing Architecture
|
|
177
218
|
To prevent centralized token expiration failures, Clinch separates ownership from routing:
|
|
@@ -241,6 +282,13 @@ Initializes the cryptographic handshake with a seller. Returns `sessionId`.
|
|
|
241
282
|
#### `exportSessionState(sessionId)` / `importSessionState(serializedData)`
|
|
242
283
|
Serializes or de-serializes all in-flight state for a given session, including ephemeral Ed25519 keys, state metrics, and local sandbox parameters, into an exchangeable JSON string.
|
|
243
284
|
|
|
285
|
+
#### `registerSecret(domain, key, name?)` / `clearSecret(domain)`
|
|
286
|
+
Saves or clears an API secret locally. The key is never visible to the AI agent and is silently passed in handshake payloads to the designated domain.
|
|
287
|
+
|
|
288
|
+
#### `async negotiateCascade(category, constraints, maxSellers?, strategy?)`
|
|
289
|
+
Queries the discovery register and cascade-negotiates with matching nodes.
|
|
290
|
+
* `strategy`: `'sequential'` (Sequential Squeeze) or `'parallel'` (Concurrent Race). Default: `'sequential'`.
|
|
291
|
+
|
|
244
292
|
#### `buildAgentPrompt(sessionId, incomingMessage)`
|
|
245
293
|
Assembles a contextual, structure-compliant system prompt for external LLMs to ensure compliant JSON execution.
|
|
246
294
|
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface SessionState {
|
|
|
25
25
|
constraints: ConstraintVector;
|
|
26
26
|
currentTurn: number;
|
|
27
27
|
lastKnownPrice: number;
|
|
28
|
+
sellerInstructions?: string | null;
|
|
28
29
|
sandboxSequence?: any;
|
|
29
30
|
sandboxSession?: any;
|
|
30
31
|
}
|
|
@@ -48,12 +49,15 @@ export declare class ClinchCore extends EventEmitter {
|
|
|
48
49
|
identityPubKey: string;
|
|
49
50
|
private activeSessions;
|
|
50
51
|
private ws;
|
|
52
|
+
private localSecrets;
|
|
51
53
|
private isSandboxMode;
|
|
52
54
|
private sandboxModelContext;
|
|
53
55
|
private sandboxMaxTurns;
|
|
54
56
|
get activeNegotiationId(): string | null;
|
|
55
57
|
constructor(config?: ClinchConfig);
|
|
56
58
|
private setStatus;
|
|
59
|
+
registerSecret(domain: string, key: string, name?: string): void;
|
|
60
|
+
clearSecret(domain: string): void;
|
|
57
61
|
initialize(cachedToken?: string): Promise<void>;
|
|
58
62
|
private getRegistryUrl;
|
|
59
63
|
private networkRequest;
|
|
@@ -69,6 +73,12 @@ export declare class ClinchCore extends EventEmitter {
|
|
|
69
73
|
negotiate(address: string, constraints: ConstraintVector): Promise<string>;
|
|
70
74
|
sendCounter(sessionId: string, price: number, reason: string): Promise<void>;
|
|
71
75
|
exitSession(sessionId: string): Promise<string>;
|
|
76
|
+
negotiateCascade(category: string, constraints: ConstraintVector, maxSellers?: number, strategy?: 'sequential' | 'parallel'): Promise<{
|
|
77
|
+
sessionId: string;
|
|
78
|
+
sellerId: string;
|
|
79
|
+
finalPrice: number;
|
|
80
|
+
} | null>;
|
|
81
|
+
private waitForSession;
|
|
72
82
|
sandbox(config?: SandboxConfig): Promise<void>;
|
|
73
83
|
private setupSandbox;
|
|
74
84
|
private handleAutomaticSandboxTurn;
|
|
@@ -85,6 +95,7 @@ export interface SellerRecord {
|
|
|
85
95
|
categories: string[];
|
|
86
96
|
capabilities: string[];
|
|
87
97
|
display_name?: string;
|
|
98
|
+
custom_instructions?: string;
|
|
88
99
|
}
|
|
89
100
|
export declare class ClinchSeller extends EventEmitter {
|
|
90
101
|
private config;
|
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ const ws_1 = __importDefault(require("ws"));
|
|
|
47
47
|
const PROTOCOL_VERSION = "0.1.0";
|
|
48
48
|
const FIREBASE_CONFIG_URL = "https://clinchprotocol.web.app/network-config.json";
|
|
49
49
|
function toHex(arr) {
|
|
50
|
-
return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
50
|
+
return Array.from(arr).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
51
51
|
}
|
|
52
52
|
function fromHex(hex) {
|
|
53
53
|
// Strips all spaces, newlines, and rogue quotes from .env strings
|
|
@@ -55,7 +55,7 @@ function fromHex(hex) {
|
|
|
55
55
|
const match = clean.match(/.{1,2}/g);
|
|
56
56
|
if (!match)
|
|
57
57
|
return new Uint8Array(0);
|
|
58
|
-
return new Uint8Array(match.map(byte => parseInt(byte, 16)));
|
|
58
|
+
return new Uint8Array(match.map((byte) => parseInt(byte, 16)));
|
|
59
59
|
}
|
|
60
60
|
// ============================================================================
|
|
61
61
|
// THE CLINCH CORE LIBRARY (Buyer)
|
|
@@ -71,6 +71,8 @@ class ClinchCore extends events_1.EventEmitter {
|
|
|
71
71
|
identityPubKey;
|
|
72
72
|
activeSessions = new Map();
|
|
73
73
|
ws = null;
|
|
74
|
+
// Blind Key Pass Local Secret Store
|
|
75
|
+
localSecrets = new Map();
|
|
74
76
|
isSandboxMode = false;
|
|
75
77
|
sandboxModelContext = null;
|
|
76
78
|
sandboxMaxTurns = 6;
|
|
@@ -99,6 +101,18 @@ class ClinchCore extends events_1.EventEmitter {
|
|
|
99
101
|
this.emit('log', `🟡 [State] ${this.status}`);
|
|
100
102
|
}
|
|
101
103
|
}
|
|
104
|
+
// --------------------------------------------------------------------------
|
|
105
|
+
// BLIND KEY PASS MANAGERS (Silent API Key Handshake)
|
|
106
|
+
// --------------------------------------------------------------------------
|
|
107
|
+
registerSecret(domain, key, name) {
|
|
108
|
+
const normalizedDomain = domain.toLowerCase().trim();
|
|
109
|
+
this.localSecrets.set(normalizedDomain, { key, name });
|
|
110
|
+
this.emit('log', `[Security] Blind key registered for ${normalizedDomain} (${name || 'Unnamed'})`);
|
|
111
|
+
}
|
|
112
|
+
clearSecret(domain) {
|
|
113
|
+
const normalizedDomain = domain.toLowerCase().trim();
|
|
114
|
+
this.localSecrets.delete(normalizedDomain);
|
|
115
|
+
}
|
|
102
116
|
async initialize(cachedToken) {
|
|
103
117
|
if (this.status === 'IDLE' || this.status === 'CONNECTING')
|
|
104
118
|
return;
|
|
@@ -243,7 +257,8 @@ class ClinchCore extends events_1.EventEmitter {
|
|
|
243
257
|
constraints: session.constraints,
|
|
244
258
|
currentTurn: session.currentTurn,
|
|
245
259
|
lastKnownPrice: session.lastKnownPrice,
|
|
246
|
-
ephemeralSecretKeyHex: toHex(session.keyPair.secretKey)
|
|
260
|
+
ephemeralSecretKeyHex: toHex(session.keyPair.secretKey),
|
|
261
|
+
sellerInstructions: session.sellerInstructions
|
|
247
262
|
};
|
|
248
263
|
return JSON.stringify(exportable);
|
|
249
264
|
}
|
|
@@ -259,7 +274,8 @@ class ClinchCore extends events_1.EventEmitter {
|
|
|
259
274
|
constraints: data.constraints,
|
|
260
275
|
currentTurn: data.currentTurn,
|
|
261
276
|
lastKnownPrice: data.lastKnownPrice,
|
|
262
|
-
keyPair: keyPair
|
|
277
|
+
keyPair: keyPair,
|
|
278
|
+
sellerInstructions: data.sellerInstructions
|
|
263
279
|
});
|
|
264
280
|
this.emit('log', `[State] Rehydrated session ${data.sessionId} pointing at ${data.sellerId}`);
|
|
265
281
|
}
|
|
@@ -272,6 +288,9 @@ class ClinchCore extends events_1.EventEmitter {
|
|
|
272
288
|
throw new Error("Cannot build prompt: Session not found.");
|
|
273
289
|
const gap = session.lastKnownPrice - session.constraints.max_budget;
|
|
274
290
|
const gapText = gap > 0 ? `-$${gap} (Over budget)` : `+$${Math.abs(gap)} (Under budget)`;
|
|
291
|
+
const customInstructionsBlock = session.sellerInstructions
|
|
292
|
+
? `\nCUSTOM INSTRUCTIONS DIRECT FROM TARGET DOMAIN (${session.sellerId}):\n"""\n${session.sellerInstructions}\n"""\n`
|
|
293
|
+
: "";
|
|
275
294
|
return `You are a professional AI purchasing agent negotiating via the Clinch Protocol.
|
|
276
295
|
Your only goal is to secure the requested item below the maximum budget.
|
|
277
296
|
|
|
@@ -282,7 +301,7 @@ NEGOTIATION STATE:
|
|
|
282
301
|
- Current turn: ${session.currentTurn}
|
|
283
302
|
- Last seller price: $${session.lastKnownPrice}
|
|
284
303
|
- Gap to budget: ${gapText}
|
|
285
|
-
|
|
304
|
+
${customInstructionsBlock}
|
|
286
305
|
SELLER'S LATEST MESSAGE:
|
|
287
306
|
"${incomingMessage}"
|
|
288
307
|
|
|
@@ -307,10 +326,11 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
307
326
|
return await this.networkRequest(url);
|
|
308
327
|
}
|
|
309
328
|
async negotiate(address, constraints) {
|
|
310
|
-
this.emit('log', `[Protocol]
|
|
329
|
+
this.emit('log', `[Protocol] Handshaking with ${address}...`);
|
|
311
330
|
const parsed = this.parseAddress(address);
|
|
312
331
|
const ephemeralKeys = tweetnacl_1.default.sign.keyPair();
|
|
313
332
|
const ephemeralPubHex = toHex(ephemeralKeys.publicKey);
|
|
333
|
+
const blindSecret = this.localSecrets.get(parsed.domain);
|
|
314
334
|
const initPayload = {
|
|
315
335
|
clinch_version: PROTOCOL_VERSION,
|
|
316
336
|
mode: parsed.mode,
|
|
@@ -318,6 +338,10 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
318
338
|
session_pub_key: ephemeralPubHex,
|
|
319
339
|
timestamp_utc: new Date().toISOString()
|
|
320
340
|
};
|
|
341
|
+
if (blindSecret) {
|
|
342
|
+
initPayload.blind_auth_token = blindSecret.key;
|
|
343
|
+
this.emit('log', `[Security] Silently injected blind token for ${parsed.domain} at transport layer`);
|
|
344
|
+
}
|
|
321
345
|
const msgUint8 = new TextEncoder().encode(JSON.stringify(initPayload));
|
|
322
346
|
const signature = toHex(tweetnacl_1.default.sign.detached(msgUint8, ephemeralKeys.secretKey));
|
|
323
347
|
const response = await this.networkRequest(`/api/route/${parsed.domain}/handshake`, {
|
|
@@ -325,6 +349,7 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
325
349
|
headers: { 'Content-Type': 'application/json' },
|
|
326
350
|
body: JSON.stringify({ ...initPayload, sig: signature })
|
|
327
351
|
});
|
|
352
|
+
const instructions = response.custom_instructions || null;
|
|
328
353
|
this.activeSessions.set(response.session_id, {
|
|
329
354
|
sessionId: response.session_id,
|
|
330
355
|
sellerId: parsed.domain,
|
|
@@ -332,7 +357,8 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
332
357
|
status: 'ACTIVE',
|
|
333
358
|
constraints,
|
|
334
359
|
currentTurn: 1,
|
|
335
|
-
lastKnownPrice: 0
|
|
360
|
+
lastKnownPrice: 0,
|
|
361
|
+
sellerInstructions: instructions
|
|
336
362
|
});
|
|
337
363
|
this.setStatus('NEGOTIATING');
|
|
338
364
|
this.emit('session_started', { sessionId: response.session_id, sellerId: parsed.domain });
|
|
@@ -368,6 +394,95 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
368
394
|
session.exitTokenHash = res.token_hash;
|
|
369
395
|
return res.token_hash;
|
|
370
396
|
}
|
|
397
|
+
async negotiateCascade(category, constraints, maxSellers = 3, strategy = 'sequential') {
|
|
398
|
+
this.emit('log', `[Cascade] Querying registry for matching sellers under "${category}"...`);
|
|
399
|
+
const discovery = await this.search(category);
|
|
400
|
+
const sellers = (discovery.results || []).slice(0, maxSellers);
|
|
401
|
+
if (sellers.length === 0) {
|
|
402
|
+
this.emit('log', `[Cascade] No matching sellers found for category: "${category}"`);
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
if (strategy === 'parallel') {
|
|
406
|
+
this.emit('log', `[Cascade] ⚡ Running PARALLEL RACE simultaneously across ${sellers.length} seller nodes...`);
|
|
407
|
+
const sessionPromises = sellers.map(async (seller) => {
|
|
408
|
+
const targetAddress = `ANP/C.${seller.agent_id}`;
|
|
409
|
+
try {
|
|
410
|
+
const sessionId = await this.negotiate(targetAddress, constraints);
|
|
411
|
+
const result = await this.waitForSession(sessionId);
|
|
412
|
+
return { sellerId: seller.agent_id, sessionId, ...result };
|
|
413
|
+
}
|
|
414
|
+
catch (e) {
|
|
415
|
+
this.emit('log', `[Cascade] ⚠️ Connection failed for parallel channel ${seller.agent_id}: ${e.message}`);
|
|
416
|
+
return { sellerId: seller.agent_id, sessionId: '', outcome: 'stalemate', price: Infinity };
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
const results = await Promise.all(sessionPromises);
|
|
420
|
+
const successfulDeals = results.filter((r) => r.outcome === 'deal' && r.price <= constraints.max_budget);
|
|
421
|
+
if (successfulDeals.length === 0) {
|
|
422
|
+
this.emit('log', `[Cascade] ✗ Parallel race completed. No successful deals reached.`);
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
successfulDeals.sort((a, b) => a.price - b.price);
|
|
426
|
+
const winner = successfulDeals[0];
|
|
427
|
+
this.emit('log', `[Cascade] 🏆 Parallel race complete! Lowest offer: $${winner.price} from ${winner.sellerId}`);
|
|
428
|
+
return {
|
|
429
|
+
sessionId: winner.sessionId,
|
|
430
|
+
sellerId: winner.sellerId,
|
|
431
|
+
finalPrice: winner.price
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
this.emit('log', `[Cascade] ➔ Running SEQUENTIAL SQUEEZE across ${sellers.length} seller nodes...`);
|
|
435
|
+
let bestDeal = null;
|
|
436
|
+
let currentBudgetCeiling = constraints.max_budget;
|
|
437
|
+
for (const seller of sellers) {
|
|
438
|
+
const targetAddress = `ANP/C.${seller.agent_id}`;
|
|
439
|
+
this.emit('log', `\n[Cascade] Squeezing target: ${targetAddress} | Squeeze Ceiling: $${currentBudgetCeiling}`);
|
|
440
|
+
const sessionConstraints = {
|
|
441
|
+
...constraints,
|
|
442
|
+
max_budget: currentBudgetCeiling
|
|
443
|
+
};
|
|
444
|
+
try {
|
|
445
|
+
const sessionId = await this.negotiate(targetAddress, sessionConstraints);
|
|
446
|
+
const result = await this.waitForSession(sessionId);
|
|
447
|
+
if (result.outcome === 'deal' && result.price < currentBudgetCeiling) {
|
|
448
|
+
this.emit('log', `[Cascade] ✓ Better deal clinched at $${result.price} from ${seller.agent_id}!`);
|
|
449
|
+
bestDeal = {
|
|
450
|
+
sessionId,
|
|
451
|
+
sellerId: seller.agent_id,
|
|
452
|
+
finalPrice: result.price
|
|
453
|
+
};
|
|
454
|
+
currentBudgetCeiling = result.price;
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
this.emit('log', `[Cascade] ✗ Seller ${seller.agent_id} failed to beat current best offer of $${currentBudgetCeiling}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
this.emit('log', `[Cascade] ⚠️ Dynamic session error with ${seller.agent_id}: ${e.message}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return bestDeal;
|
|
465
|
+
}
|
|
466
|
+
waitForSession(sessionId) {
|
|
467
|
+
return new Promise((resolve) => {
|
|
468
|
+
const onClosed = (event) => {
|
|
469
|
+
if (event.sessionId === sessionId) {
|
|
470
|
+
this.off('session_closed', onClosed);
|
|
471
|
+
this.off('status_changed', onStatus);
|
|
472
|
+
resolve({ outcome: 'deal', price: event.finalPrice });
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
const onStatus = (status) => {
|
|
476
|
+
if (status === 'STALEMATE') {
|
|
477
|
+
this.off('session_closed', onClosed);
|
|
478
|
+
this.off('status_changed', onStatus);
|
|
479
|
+
resolve({ outcome: 'stalemate', price: Infinity });
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
this.on('session_closed', onClosed);
|
|
483
|
+
this.on('status_changed', onStatus);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
371
486
|
async sandbox(config = {}) {
|
|
372
487
|
this.isSandboxMode = true;
|
|
373
488
|
this.sandboxMaxTurns = config.maxTurns || 6;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ const PROTOCOL_VERSION = "0.1.0";
|
|
|
10
10
|
const FIREBASE_CONFIG_URL = "https://clinchprotocol.web.app/network-config.json";
|
|
11
11
|
|
|
12
12
|
function toHex(arr: Uint8Array | number[]): string {
|
|
13
|
-
return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
13
|
+
return Array.from(arr).map((b: number) => b.toString(16).padStart(2, '0')).join('');
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function fromHex(hex: string): Uint8Array {
|
|
@@ -18,7 +18,7 @@ function fromHex(hex: string): Uint8Array {
|
|
|
18
18
|
const clean = hex.replace(/[^0-9a-fA-F]/g, '');
|
|
19
19
|
const match = clean.match(/.{1,2}/g);
|
|
20
20
|
if (!match) return new Uint8Array(0);
|
|
21
|
-
return new Uint8Array(match.map(byte => parseInt(byte, 16)));
|
|
21
|
+
return new Uint8Array(match.map((byte: string) => parseInt(byte, 16)));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// ============================================================================
|
|
@@ -63,6 +63,9 @@ export interface SessionState {
|
|
|
63
63
|
currentTurn: number;
|
|
64
64
|
lastKnownPrice: number;
|
|
65
65
|
|
|
66
|
+
// Direct, dynamic instructions injected by the target domain
|
|
67
|
+
sellerInstructions?: string | null;
|
|
68
|
+
|
|
66
69
|
sandboxSequence?: any;
|
|
67
70
|
sandboxSession?: any;
|
|
68
71
|
}
|
|
@@ -97,6 +100,9 @@ export class ClinchCore extends EventEmitter {
|
|
|
97
100
|
private activeSessions = new Map<string, SessionState>();
|
|
98
101
|
private ws: WebSocket | null = null;
|
|
99
102
|
|
|
103
|
+
// Blind Key Pass Local Secret Store
|
|
104
|
+
private localSecrets = new Map<string, { key: string, name?: string }>();
|
|
105
|
+
|
|
100
106
|
private isSandboxMode = false;
|
|
101
107
|
private sandboxModelContext: any = null;
|
|
102
108
|
private sandboxMaxTurns = 6;
|
|
@@ -129,6 +135,20 @@ export class ClinchCore extends EventEmitter {
|
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
// --------------------------------------------------------------------------
|
|
139
|
+
// BLIND KEY PASS MANAGERS (Silent API Key Handshake)
|
|
140
|
+
// --------------------------------------------------------------------------
|
|
141
|
+
public registerSecret(domain: string, key: string, name?: string): void {
|
|
142
|
+
const normalizedDomain = domain.toLowerCase().trim();
|
|
143
|
+
this.localSecrets.set(normalizedDomain, { key, name });
|
|
144
|
+
this.emit('log', `[Security] Blind key registered for ${normalizedDomain} (${name || 'Unnamed'})`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public clearSecret(domain: string): void {
|
|
148
|
+
const normalizedDomain = domain.toLowerCase().trim();
|
|
149
|
+
this.localSecrets.delete(normalizedDomain);
|
|
150
|
+
}
|
|
151
|
+
|
|
132
152
|
async initialize(cachedToken?: string): Promise<void> {
|
|
133
153
|
if (this.status === 'IDLE' || this.status === 'CONNECTING') return;
|
|
134
154
|
this.setStatus('CONNECTING');
|
|
@@ -278,7 +298,8 @@ export class ClinchCore extends EventEmitter {
|
|
|
278
298
|
constraints: session.constraints,
|
|
279
299
|
currentTurn: session.currentTurn,
|
|
280
300
|
lastKnownPrice: session.lastKnownPrice,
|
|
281
|
-
ephemeralSecretKeyHex: toHex(session.keyPair.secretKey)
|
|
301
|
+
ephemeralSecretKeyHex: toHex(session.keyPair.secretKey),
|
|
302
|
+
sellerInstructions: session.sellerInstructions
|
|
282
303
|
};
|
|
283
304
|
|
|
284
305
|
return JSON.stringify(exportable);
|
|
@@ -297,7 +318,8 @@ export class ClinchCore extends EventEmitter {
|
|
|
297
318
|
constraints: data.constraints,
|
|
298
319
|
currentTurn: data.currentTurn,
|
|
299
320
|
lastKnownPrice: data.lastKnownPrice,
|
|
300
|
-
keyPair: keyPair
|
|
321
|
+
keyPair: keyPair,
|
|
322
|
+
sellerInstructions: data.sellerInstructions
|
|
301
323
|
});
|
|
302
324
|
|
|
303
325
|
this.emit('log', `[State] Rehydrated session ${data.sessionId} pointing at ${data.sellerId}`);
|
|
@@ -314,6 +336,10 @@ export class ClinchCore extends EventEmitter {
|
|
|
314
336
|
const gap = session.lastKnownPrice - session.constraints.max_budget;
|
|
315
337
|
const gapText = gap > 0 ? `-$${gap} (Over budget)` : `+$${Math.abs(gap)} (Under budget)`;
|
|
316
338
|
|
|
339
|
+
const customInstructionsBlock = session.sellerInstructions
|
|
340
|
+
? `\nCUSTOM INSTRUCTIONS DIRECT FROM TARGET DOMAIN (${session.sellerId}):\n"""\n${session.sellerInstructions}\n"""\n`
|
|
341
|
+
: "";
|
|
342
|
+
|
|
317
343
|
return `You are a professional AI purchasing agent negotiating via the Clinch Protocol.
|
|
318
344
|
Your only goal is to secure the requested item below the maximum budget.
|
|
319
345
|
|
|
@@ -324,7 +350,7 @@ NEGOTIATION STATE:
|
|
|
324
350
|
- Current turn: ${session.currentTurn}
|
|
325
351
|
- Last seller price: $${session.lastKnownPrice}
|
|
326
352
|
- Gap to budget: ${gapText}
|
|
327
|
-
|
|
353
|
+
${customInstructionsBlock}
|
|
328
354
|
SELLER'S LATEST MESSAGE:
|
|
329
355
|
"${incomingMessage}"
|
|
330
356
|
|
|
@@ -350,13 +376,15 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
350
376
|
}
|
|
351
377
|
|
|
352
378
|
async negotiate(address: string, constraints: ConstraintVector): Promise<string> {
|
|
353
|
-
this.emit('log', `[Protocol]
|
|
379
|
+
this.emit('log', `[Protocol] Handshaking with ${address}...`);
|
|
354
380
|
const parsed = this.parseAddress(address);
|
|
355
381
|
|
|
356
382
|
const ephemeralKeys = nacl.sign.keyPair();
|
|
357
383
|
const ephemeralPubHex = toHex(ephemeralKeys.publicKey);
|
|
358
384
|
|
|
359
|
-
const
|
|
385
|
+
const blindSecret = this.localSecrets.get(parsed.domain);
|
|
386
|
+
|
|
387
|
+
const initPayload: any = {
|
|
360
388
|
clinch_version: PROTOCOL_VERSION,
|
|
361
389
|
mode: parsed.mode,
|
|
362
390
|
constraints,
|
|
@@ -364,6 +392,11 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
364
392
|
timestamp_utc: new Date().toISOString()
|
|
365
393
|
};
|
|
366
394
|
|
|
395
|
+
if (blindSecret) {
|
|
396
|
+
initPayload.blind_auth_token = blindSecret.key;
|
|
397
|
+
this.emit('log', `[Security] Silently injected blind token for ${parsed.domain} at transport layer`);
|
|
398
|
+
}
|
|
399
|
+
|
|
367
400
|
const msgUint8 = new TextEncoder().encode(JSON.stringify(initPayload));
|
|
368
401
|
const signature = toHex(nacl.sign.detached(msgUint8, ephemeralKeys.secretKey));
|
|
369
402
|
|
|
@@ -373,6 +406,8 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
373
406
|
body: JSON.stringify({ ...initPayload, sig: signature })
|
|
374
407
|
});
|
|
375
408
|
|
|
409
|
+
const instructions = response.custom_instructions || null;
|
|
410
|
+
|
|
376
411
|
this.activeSessions.set(response.session_id, {
|
|
377
412
|
sessionId: response.session_id,
|
|
378
413
|
sellerId: parsed.domain,
|
|
@@ -380,7 +415,8 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
380
415
|
status: 'ACTIVE',
|
|
381
416
|
constraints,
|
|
382
417
|
currentTurn: 1,
|
|
383
|
-
lastKnownPrice: 0
|
|
418
|
+
lastKnownPrice: 0,
|
|
419
|
+
sellerInstructions: instructions
|
|
384
420
|
});
|
|
385
421
|
|
|
386
422
|
this.setStatus('NEGOTIATING');
|
|
@@ -426,6 +462,112 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
426
462
|
return res.token_hash;
|
|
427
463
|
}
|
|
428
464
|
|
|
465
|
+
public async negotiateCascade(
|
|
466
|
+
category: string,
|
|
467
|
+
constraints: ConstraintVector,
|
|
468
|
+
maxSellers = 3,
|
|
469
|
+
strategy: 'sequential' | 'parallel' = 'sequential'
|
|
470
|
+
): Promise<{ sessionId: string, sellerId: string, finalPrice: number } | null> {
|
|
471
|
+
this.emit('log', `[Cascade] Querying registry for matching sellers under "${category}"...`);
|
|
472
|
+
const discovery = await this.search(category);
|
|
473
|
+
const sellers: any[] = (discovery.results || []).slice(0, maxSellers);
|
|
474
|
+
|
|
475
|
+
if (sellers.length === 0) {
|
|
476
|
+
this.emit('log', `[Cascade] No matching sellers found for category: "${category}"`);
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (strategy === 'parallel') {
|
|
481
|
+
this.emit('log', `[Cascade] ⚡ Running PARALLEL RACE simultaneously across ${sellers.length} seller nodes...`);
|
|
482
|
+
|
|
483
|
+
const sessionPromises = sellers.map(async (seller: any) => {
|
|
484
|
+
const targetAddress = `ANP/C.${seller.agent_id}`;
|
|
485
|
+
try {
|
|
486
|
+
const sessionId = await this.negotiate(targetAddress, constraints);
|
|
487
|
+
const result = await this.waitForSession(sessionId);
|
|
488
|
+
return { sellerId: seller.agent_id, sessionId, ...result };
|
|
489
|
+
} catch (e: any) {
|
|
490
|
+
this.emit('log', `[Cascade] ⚠️ Connection failed for parallel channel ${seller.agent_id}: ${e.message}`);
|
|
491
|
+
return { sellerId: seller.agent_id, sessionId: '', outcome: 'stalemate' as const, price: Infinity };
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const results = await Promise.all(sessionPromises);
|
|
496
|
+
const successfulDeals = results.filter((r: any) => r.outcome === 'deal' && r.price <= constraints.max_budget);
|
|
497
|
+
|
|
498
|
+
if (successfulDeals.length === 0) {
|
|
499
|
+
this.emit('log', `[Cascade] ✗ Parallel race completed. No successful deals reached.`);
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
successfulDeals.sort((a: any, b: any) => a.price - b.price);
|
|
504
|
+
const winner = successfulDeals[0];
|
|
505
|
+
|
|
506
|
+
this.emit('log', `[Cascade] 🏆 Parallel race complete! Lowest offer: $${winner.price} from ${winner.sellerId}`);
|
|
507
|
+
return {
|
|
508
|
+
sessionId: winner.sessionId,
|
|
509
|
+
sellerId: winner.sellerId,
|
|
510
|
+
finalPrice: winner.price
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
this.emit('log', `[Cascade] ➔ Running SEQUENTIAL SQUEEZE across ${sellers.length} seller nodes...`);
|
|
515
|
+
let bestDeal: { sessionId: string, sellerId: string, finalPrice: number } | null = null;
|
|
516
|
+
let currentBudgetCeiling = constraints.max_budget;
|
|
517
|
+
|
|
518
|
+
for (const seller of sellers) {
|
|
519
|
+
const targetAddress = `ANP/C.${seller.agent_id}`;
|
|
520
|
+
this.emit('log', `\n[Cascade] Squeezing target: ${targetAddress} | Squeeze Ceiling: $${currentBudgetCeiling}`);
|
|
521
|
+
|
|
522
|
+
const sessionConstraints = {
|
|
523
|
+
...constraints,
|
|
524
|
+
max_budget: currentBudgetCeiling
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
const sessionId = await this.negotiate(targetAddress, sessionConstraints);
|
|
529
|
+
const result = await this.waitForSession(sessionId);
|
|
530
|
+
|
|
531
|
+
if (result.outcome === 'deal' && result.price < currentBudgetCeiling) {
|
|
532
|
+
this.emit('log', `[Cascade] ✓ Better deal clinched at $${result.price} from ${seller.agent_id}!`);
|
|
533
|
+
bestDeal = {
|
|
534
|
+
sessionId,
|
|
535
|
+
sellerId: seller.agent_id,
|
|
536
|
+
finalPrice: result.price
|
|
537
|
+
};
|
|
538
|
+
currentBudgetCeiling = result.price;
|
|
539
|
+
} else {
|
|
540
|
+
this.emit('log', `[Cascade] ✗ Seller ${seller.agent_id} failed to beat current best offer of $${currentBudgetCeiling}`);
|
|
541
|
+
}
|
|
542
|
+
} catch (e: any) {
|
|
543
|
+
this.emit('log', `[Cascade] ⚠️ Dynamic session error with ${seller.agent_id}: ${e.message}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return bestDeal;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private waitForSession(sessionId: string): Promise<{ outcome: 'deal' | 'stalemate', price: number }> {
|
|
551
|
+
return new Promise((resolve) => {
|
|
552
|
+
const onClosed = (event: any) => {
|
|
553
|
+
if (event.sessionId === sessionId) {
|
|
554
|
+
this.off('session_closed', onClosed);
|
|
555
|
+
this.off('status_changed', onStatus);
|
|
556
|
+
resolve({ outcome: 'deal', price: event.finalPrice });
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
const onStatus = (status: CoreStatus) => {
|
|
560
|
+
if (status === 'STALEMATE') {
|
|
561
|
+
this.off('session_closed', onClosed);
|
|
562
|
+
this.off('status_changed', onStatus);
|
|
563
|
+
resolve({ outcome: 'stalemate', price: Infinity });
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
this.on('session_closed', onClosed);
|
|
567
|
+
this.on('status_changed', onStatus);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
429
571
|
async sandbox(config: SandboxConfig = {}): Promise<void> {
|
|
430
572
|
this.isSandboxMode = true;
|
|
431
573
|
this.sandboxMaxTurns = config.maxTurns || 6;
|
|
@@ -600,12 +742,13 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
600
742
|
// THE CLINCH SELLER LIBRARY (Server-Side)
|
|
601
743
|
// ============================================================================
|
|
602
744
|
export interface SellerRecord {
|
|
603
|
-
agent_id:
|
|
604
|
-
endpoint:
|
|
605
|
-
supported_modes:
|
|
606
|
-
categories:
|
|
607
|
-
capabilities:
|
|
608
|
-
display_name?:
|
|
745
|
+
agent_id: string;
|
|
746
|
+
endpoint: string;
|
|
747
|
+
supported_modes: string[];
|
|
748
|
+
categories: string[];
|
|
749
|
+
capabilities: string[];
|
|
750
|
+
display_name?: string;
|
|
751
|
+
custom_instructions?: string; // Strictly typed dynamic record instructions mapping
|
|
609
752
|
}
|
|
610
753
|
|
|
611
754
|
export class ClinchSeller extends EventEmitter {
|